ch_connect 0.3.1 → 0.4.1

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.
@@ -68,6 +68,21 @@
68
68
  # define CHC_REPRODUCIBLE
69
69
  #endif
70
70
 
71
+ #if CHC__HAS_ATTR(fallthrough)
72
+ # define CHC_FALLTHROUGH [[fallthrough]]
73
+ #else
74
+ # define CHC_FALLTHROUGH
75
+ #endif
76
+
77
+ /* unreachable() is C23 <stddef.h>, already included above. */
78
+ #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
79
+ # define CHC_UNREACHABLE() unreachable()
80
+ #elif defined(__GNUC__) || defined(__clang__)
81
+ # define CHC_UNREACHABLE() __builtin_unreachable()
82
+ #else
83
+ # define CHC_UNREACHABLE() ((void) 0)
84
+ #endif
85
+
71
86
  /* ckd_mul (C23 <stdckdint.h>) backs chc__mul_size; see CHC__HAVE_CKD_MUL. */
72
87
  #if defined(__has_include)
73
88
  # if __has_include(<stdckdint.h>)
@@ -102,16 +117,11 @@ enum {
102
117
  #endif
103
118
 
104
119
  typedef struct chc_err {
105
- int server_code;
106
120
  char msg[CHC_ERR_MSG_LEN];
107
- char server_name[64];
108
121
  } chc_err;
109
122
 
110
123
  static inline void chc_err_reset(chc_err *e) {
111
- if (!e) return;
112
- e->server_code = 0;
113
- e->msg[0] = '\0';
114
- e->server_name[0] = '\0';
124
+ if (e) e->msg[0] = '\0';
115
125
  }
116
126
 
117
127
  /* -------------------------------------------------------------------------- */
@@ -180,7 +190,8 @@ typedef enum chc_kind {
180
190
  CHC_NULLABLE, CHC_ARRAY, CHC_TUPLE, CHC_MAP, CHC_NESTED,
181
191
  CHC_LOW_CARDINALITY,
182
192
  CHC_INTERVAL,
183
- CHC_POINT, CHC_RING, CHC_POLYGON, CHC_MULTI_POLYGON,
193
+ CHC_POINT, CHC_RING, CHC_LINE_STRING,
194
+ CHC_POLYGON, CHC_MULTI_POLYGON, CHC_MULTI_LINE_STRING,
184
195
  CHC_VARIANT, CHC_DYNAMIC, CHC_JSON, CHC_OBJECT,
185
196
  CHC_AGGREGATE_FUNCTION, CHC_SIMPLE_AGGREGATE_FUNCTION,
186
197
  CHC_QBIT,
@@ -188,6 +199,22 @@ typedef enum chc_kind {
188
199
  CHC_KIND_COUNT
189
200
  } chc_kind;
190
201
 
202
+ /* Interval unit, ordered as ClickHouse IntervalKind */
203
+ typedef enum chc_interval_unit {
204
+ CHC_INTERVAL_NONE = 0,
205
+ CHC_INTERVAL_NANOSECOND,
206
+ CHC_INTERVAL_MICROSECOND,
207
+ CHC_INTERVAL_MILLISECOND,
208
+ CHC_INTERVAL_SECOND,
209
+ CHC_INTERVAL_MINUTE,
210
+ CHC_INTERVAL_HOUR,
211
+ CHC_INTERVAL_DAY,
212
+ CHC_INTERVAL_WEEK,
213
+ CHC_INTERVAL_MONTH,
214
+ CHC_INTERVAL_QUARTER,
215
+ CHC_INTERVAL_YEAR,
216
+ } chc_interval_unit;
217
+
191
218
  typedef struct chc_type chc_type;
192
219
 
193
220
  CHC_NODISCARD int chc_type_parse(const char *name, size_t name_len,
@@ -204,6 +231,9 @@ int chc_type_decimal_precision(const chc_type *t);
204
231
  int chc_type_decimal_scale(const chc_type *t);
205
232
  int chc_type_datetime64_scale(const chc_type *t);
206
233
 
234
+ /* Values arrive as Int64 ticks of unit. */
235
+ chc_interval_unit chc_type_interval_unit(const chc_type *t);
236
+
207
237
  /* QBit(T, N): N (vector dimension). 0 on non-QBit types. The element type
208
238
  * (BFloat16/Float32/Float64) is children[0], reached via chc_type_child(t, 0). */
209
239
  size_t chc_type_qbit_dimension(const chc_type *t);
@@ -218,11 +248,14 @@ void chc_type_enum_at(const chc_type *t, size_t i,
218
248
  const char **name, size_t *name_len,
219
249
  int64_t *value);
220
250
 
221
- /* For Tuple types: returns the ith child's field name or NULL when the
222
- * tuple is anonymous (or i is out of range). NULL on non-Tuple types. */
251
+ /* Return a Tuple or Nested field name, or NULL for unnamed fields,
252
+ * invalid indexes, or other types */
223
253
  const char *chc_type_tuple_field_name(const chc_type *t, size_t i,
224
254
  size_t *out_len);
225
255
 
256
+ /* Return an aggregate function name without parameters, or NULL when absent */
257
+ const char *chc_type_agg_function(const chc_type *t, size_t *out_len);
258
+
226
259
  /* Reproduce the printable type name into buf. Returns the number of bytes
227
260
  * that would have been written (snprintf-style); use to size buf on a
228
261
  * second pass when the return value >= buf_len. buf may be NULL when
@@ -245,6 +278,23 @@ typedef enum chc_col_kind {
245
278
 
246
279
  typedef struct chc_column chc_column;
247
280
 
281
+ /* Column tree. Reader allocates & owns instances, freed by chc_block_destroy.
282
+ * chc_build_* initializes caller-owned instances, usually on stack, over
283
+ * caller slabs. Pointers use host byte order; writer converts to LE on BE
284
+ * hosts. Access through helpers below or fields directly */
285
+ struct chc_column {
286
+ chc_col_kind layout;
287
+ size_t n_rows;
288
+ union {
289
+ struct { void *data; size_t elem_size; } fixed;
290
+ struct { uint8_t *data; uint64_t *offsets; size_t bytes; } str;
291
+ struct { uint8_t *null_map; chc_column *inner; } nullable;
292
+ struct { uint64_t *offsets; chc_column *values; } array;
293
+ struct { chc_column **children; size_t arity; } tuple;
294
+ struct { int key_size; void *keys; chc_column *dict; size_t dict_n; } lc;
295
+ };
296
+ };
297
+
248
298
  chc_col_kind chc_column_layout(const chc_column *c);
249
299
  size_t chc_column_n_rows(const chc_column *c);
250
300
 
@@ -283,10 +333,10 @@ const chc_column *chc_column_lc_dict(const chc_column *c);
283
333
 
284
334
  /* Walk a column tree & enforce cross-field invariants the server itself
285
335
  * enforces on its native deserialization path:
286
- * - Array offsets non-decreasing (SerializationArray.cpp:444, throws
287
- * "Arrays offsets are not monotonically increasing")
288
- * - LowCardinality keys < dict size (ColumnLowCardinality.cpp:255, throws
289
- * "Index for LowCardinality is out of range")
336
+ * - Array offsets non-decreasing
337
+ * ("Arrays offsets are not monotonically increasing")
338
+ * - LowCardinality keys < dict size
339
+ * ("Index for LowCardinality is out of range")
290
340
  * chc_block_read does NOT call this automatically — a peer that forges
291
341
  * offsets or LC keys can cause callers to read past inner-column bounds.
292
342
  * Consumers ingesting from untrusted senders should call this on each
@@ -302,13 +352,14 @@ CHC_NODISCARD int chc_column_validate(const chc_column *c, chc_err *err);
302
352
  typedef struct chc_block chc_block;
303
353
 
304
354
  typedef struct chc_block_opts {
305
- /* TCP path (server_revision >= 51903): an 8-byte BlockInfo prefix is on
306
- * the wire before num_columns. clickhouse-local does not emit it. */
355
+ /* TCP path: an 8-byte BlockInfo prefix is on the wire before
356
+ * num_columns. clickhouse-local does not emit it. */
307
357
  bool has_block_info;
308
358
 
309
- /* TCP path (server_revision >= 54454): a 1-byte has_custom_serialization
310
- * flag follows each column's type name. clickhouse-local does not emit
311
- * it. */
359
+ /* TCP path: a has_custom_serialization byte follows each column's type
360
+ * name, with serialization kinds when set. Reader decodes sparse
361
+ * columns into dense layouts, writer always emits 0. clickhouse-local
362
+ * does not emit it. */
312
363
  bool has_custom_serialization;
313
364
 
314
365
  /* Internal read-buffer size. 0 = default (8 KiB). */
@@ -339,124 +390,67 @@ int32_t chc_block_bucket_num(const chc_block *b);
339
390
  /* Block writer */
340
391
  /* -------------------------------------------------------------------------- */
341
392
 
342
- typedef struct chc_block_builder chc_block_builder;
343
-
344
- CHC_NODISCARD int chc_block_builder_init(chc_block_builder **out, const chc_alloc *al,
345
- chc_err *err);
346
- void chc_block_builder_destroy(chc_block_builder *bb);
347
-
348
- /* For variable-length columns, offsets[i] is the cumulative end of row i
349
- * (exclusive ends, host byte order). For fixed columns, data is n_rows *
350
- * elem_size little-endian bytes. None of the slabs are copied; they must
351
- * outlive chc_block_write. */
352
- CHC_NODISCARD int chc_block_builder_append_fixed(chc_block_builder *bb,
353
- const char *name, size_t name_len,
354
- const chc_type *t,
355
- const void *data, size_t n_rows,
356
- chc_err *err);
357
-
358
- CHC_NODISCARD int chc_block_builder_append_string(chc_block_builder *bb,
359
- const char *name, size_t name_len,
360
- const uint64_t *offsets,
361
- const uint8_t *data, size_t n_rows,
362
- chc_err *err);
363
-
364
- /* Composite append helpers. Slabs stay caller-owned; the builder never
365
- * copies. Offsets / keys are host byte order; the writer byte-swaps to
366
- * little-endian on BE hosts. `t` carries the column's full CH type and
367
- * must match the helper variant (e.g. _nullable_fixed expects
368
- * Nullable(<fixed>), _array_string expects Array(String), and
369
- * _low_cardinality_string expects LowCardinality(String) or
370
- * LowCardinality(Nullable(String))).
393
+ /* Block column: name, full CH type & column tree. Build tree with chc_build_*
394
+ * or reuse reader output for round-trip. Tree stays caller-owned & must
395
+ * outlive write */
396
+ typedef struct {
397
+ const char *name;
398
+ size_t name_len;
399
+ const chc_type *type;
400
+ const chc_column *col;
401
+ } chc_block_col;
402
+
403
+ /* Stack builder over caller-provided chc_block_col storage. Initialize with the
404
+ * array, append columns, then call chc_block_write. Caller sizes the array;
405
+ * appending more columns than it holds is undefined. Skip builder by passing an
406
+ * array directly to chc_block_write_cols */
407
+ typedef struct chc_block_builder {
408
+ chc_block_col *cols; /* caller-owned storage */
409
+ size_t n_cols;
410
+ size_t n_rows; /* shared across columns */
411
+ } chc_block_builder;
412
+
413
+ void chc_block_builder_init(chc_block_builder *bb, chc_block_col *cols);
414
+
415
+ /* Build column trees over caller-owned slabs without copying. Children must
416
+ * outlive write. Nest constructors to match any composite reader emits, e.g.
417
+ * Array(LowCardinality(Nullable(String))):
371
418
  *
372
- * Nested arrays (Array(Array(T))) and Tuple columns are not exposed yet —
373
- * add when a consumer asks. */
374
- CHC_NODISCARD int chc_block_builder_append_nullable_fixed(
375
- chc_block_builder *bb,
376
- const char *name, size_t name_len,
377
- const chc_type *t,
378
- const uint8_t *null_map,
379
- const void *inner_data,
380
- size_t n_rows, chc_err *err);
381
-
382
- CHC_NODISCARD int chc_block_builder_append_nullable_string(
383
- chc_block_builder *bb,
384
- const char *name, size_t name_len,
385
- const chc_type *t,
386
- const uint8_t *null_map,
387
- const uint64_t *inner_offsets,
388
- const uint8_t *inner_data,
389
- size_t n_rows, chc_err *err);
390
-
391
- CHC_NODISCARD int chc_block_builder_append_array_fixed(
392
- chc_block_builder *bb,
393
- const char *name, size_t name_len,
394
- const chc_type *t,
395
- const uint64_t *offsets,
396
- const void *values,
397
- size_t n_rows, chc_err *err);
398
-
399
- CHC_NODISCARD int chc_block_builder_append_array_string(
400
- chc_block_builder *bb,
401
- const char *name, size_t name_len,
402
- const chc_type *t,
403
- const uint64_t *offsets,
404
- const uint64_t *values_offsets,
405
- const uint8_t *values_data,
406
- size_t n_rows, chc_err *err);
407
-
408
- /* Nested Array(Array(...(<fixed/string>))) variants. `t` is top-level
409
- * Array type, `ndim` is nesting depth (must match `t`). level_offsets
410
- * is ndim cumulative-end arrays ordered outer-to-inner, level_offsets_len
411
- * gives count at each level. n_rows is top-level row count, must equal
412
- * level_offsets_len[0] */
413
- CHC_NODISCARD int chc_block_builder_append_array_nested_fixed(
414
- chc_block_builder *bb,
415
- const char *name, size_t name_len,
416
- const chc_type *t,
417
- int ndim,
418
- const uint64_t * const *level_offsets,
419
- const size_t *level_offsets_len,
420
- const void *values,
421
- size_t n_rows, chc_err *err);
422
-
423
- CHC_NODISCARD int chc_block_builder_append_array_nested_string(
424
- chc_block_builder *bb,
425
- const char *name, size_t name_len,
426
- const chc_type *t,
427
- int ndim,
428
- const uint64_t * const *level_offsets,
429
- const size_t *level_offsets_len,
430
- const uint64_t *values_offsets,
431
- const uint8_t *values_data,
432
- size_t n_rows, chc_err *err);
433
-
434
- /* LowCardinality(String) or LowCardinality(Nullable(String)). For the
435
- * Nullable variant the caller must place a null-sentinel entry at dict
436
- * index 0 (CH convention) and use key 0 for null rows. */
437
- /* JSON column, STRING serialization. `t` must be CHC_JSON. Rows are JSON
438
- * document text, one per offset; builder emits an 8-byte LE serialization-
439
- * version prefix (value 1) once before the same wire format as
440
- * chc_block_builder_append_string. Caller is responsible for the input
441
- * being valid JSON; server rejects malformed documents at INSERT time. */
442
- CHC_NODISCARD int chc_block_builder_append_json_string(
443
- chc_block_builder *bb,
444
- const char *name, size_t name_len,
445
- const chc_type *t, /* CHC_JSON */
446
- const uint64_t *offsets,
447
- const uint8_t *data,
448
- size_t n_rows, chc_err *err);
449
-
450
- CHC_NODISCARD int chc_block_builder_append_low_cardinality_string(
451
- chc_block_builder *bb,
452
- const char *name, size_t name_len,
453
- const chc_type *t,
454
- int key_size,
455
- const void *keys,
456
- const uint64_t *dict_offsets,
457
- const uint8_t *dict_data,
458
- size_t dict_n,
459
- size_t n_rows, chc_err *err);
419
+ * chc_column d = chc_build_string(dict_offs, dict_data, dict_n);
420
+ * chc_column n = chc_build_nullable(dict_null_map, &d);
421
+ * chc_column l = chc_build_lc(key_size, keys, leaf_rows, &n);
422
+ * chc_column a = chc_build_array(offsets, n_rows, &l);
423
+ * chc_block_builder_append(bb, "c", 1, t, &a);
424
+ *
425
+ * Each node stores row count for its level. Fixed, string, array & LC builders
426
+ * receive it explicitly; nullable & tuple builders derive it from children.
427
+ * Array leaf count equals offsets[n_rows - 1]; LC leaf count equals key count.
428
+ * Fixed data contains n_rows * elem_size LE bytes; string offsets contain
429
+ * cumulative exclusive ends. For LC(Nullable(T)), supply inner-typed dict
430
+ * with null sentinel at slot 0 or Nullable wrapper. Plain String, FixedString,
431
+ * JSON & Object need one matching chc_build_fixed or chc_build_string */
432
+ chc_column chc_build_fixed(const void *data, size_t elem_size, size_t n_rows);
433
+ chc_column chc_build_string(const uint64_t *offsets, const uint8_t *data,
434
+ size_t n_rows);
435
+ chc_column chc_build_nullable(const uint8_t *null_map, chc_column *inner);
436
+ chc_column chc_build_array(const uint64_t *offsets, size_t n_rows,
437
+ chc_column *values);
438
+ chc_column chc_build_tuple(chc_column **children, size_t arity);
439
+ chc_column chc_build_lc(int key_size, const void *keys, size_t n_rows,
440
+ chc_column *dict);
441
+
442
+ /* Append column. `t` gives full CH type; `col` must match structurally & share
443
+ * the block row count, both checked during write. Caller must not exceed the
444
+ * storage passed to chc_block_builder_init */
445
+ void chc_block_builder_append(chc_block_builder *bb,
446
+ const char *name, size_t name_len,
447
+ const chc_type *t, const chc_column *col);
448
+
449
+ /* Write directly from caller-built columns. Require cols[i].col->n_rows ==
450
+ * n_rows for every column */
451
+ CHC_NODISCARD int chc_block_write_cols(chc_io *io, const chc_block_col *cols,
452
+ size_t n_cols, size_t n_rows,
453
+ const chc_block_opts *opts, chc_err *err);
460
454
 
461
455
  CHC_NODISCARD int chc_block_write(chc_io *io, const chc_block_builder *bb,
462
456
  const chc_block_opts *opts, chc_err *err);
@@ -504,10 +498,10 @@ static inline uint64_t chc__bswap64(uint64_t v) {
504
498
  /* -------- CityHash short-string helpers ---------- */
505
499
 
506
500
  /* Frozen v1.0.3 variant of CityHash, ported from city.cc.
507
- * Original: Copyright (c) 2011 Google, Inc. (MIT licence).
508
- * Short-string path lives here so chc__name_to_kind can reuse it; the
509
- * 128-bit driver used by compressed-frame checksums sits in
510
- * clickhouse-compression.h and builds on these helpers. */
501
+ * Original: Copyright (c) 2011 Google, Inc. (MIT license).
502
+ * Short-string path lives here so chc__name_lookup can reuse it; 128-bit
503
+ * compressed-frame checksum driver in clickhouse-compression.h builds on
504
+ * these helpers */
511
505
 
512
506
  static uint64_t chc__city_fetch64(const char *p) CHC_REPRODUCIBLE
513
507
  {
@@ -596,14 +590,10 @@ static int CHC__PRINTF_FMT(3, 4)
596
590
  chc__err_set(chc_err *e, int code, const char *fmt, ...)
597
591
  {
598
592
  if (!e) return code;
599
- if (fmt) {
600
- va_list ap;
601
- __builtin_va_start(ap, fmt);
602
- vsnprintf(e->msg, sizeof e->msg, fmt, ap);
603
- __builtin_va_end(ap);
604
- } else {
605
- e->msg[0] = '\0';
606
- }
593
+ va_list ap;
594
+ va_start(ap, fmt);
595
+ vsnprintf(e->msg, sizeof e->msg, fmt, ap);
596
+ va_end(ap);
607
597
  return code;
608
598
  }
609
599
 
@@ -731,11 +721,7 @@ chc_alloc chc_alloc_stdlib(void) {
731
721
  #define CHC_READ_BUFFER 8192
732
722
  #endif
733
723
 
734
- /* Mirror ClickHouse's limits:
735
- * https://github.com/ClickHouse/ClickHouse/blob/ef11941cf5a/src/IO/ReadHelpers.h#L38
736
- * https://github.com/ClickHouse/ClickHouse/blob/ef11941cf5a/src/Core/Defines.h#L156-L158
737
- * https://github.com/ClickHouse/ClickHouse/blob/ef11941cf5a/src/DataTypes/DataTypeFixedString.h#L5
738
- * https://github.com/ClickHouse/ClickHouse/blob/ef11941cf5a/src/DataTypes/DataTypeFactory.cpp#L122-L127 */
724
+ /* Mirrors the limits ClickHouse enforces server-side. */
739
725
  #ifndef CHC_MAX_STRING_SIZE
740
726
  #define CHC_MAX_STRING_SIZE (1ULL << 30)
741
727
  #endif
@@ -795,9 +781,9 @@ chc_in_init_ioless(chc_in *in, const chc_alloc *al)
795
781
  return CHC_OK;
796
782
  }
797
783
 
798
- /* Drop prefix [0, keep): keep = mark when a checkpoint is live, else pos.
799
- * consumed counts returned bytes, not offsets, so compaction leaves it be;
800
- * mark and pos shift together so (pos - mark) survives for rewind. */
784
+ /* Drop prefix [0, keep), using mark while checkpoint is live, otherwise pos.
785
+ * consumed tracks returned bytes rather than offsets, so compaction leaves it
786
+ * unchanged; shifting mark & pos preserves pos - mark for rewind */
801
787
  static void
802
788
  chc__in_compact(chc_in *in)
803
789
  {
@@ -853,8 +839,8 @@ chc_in_free(chc_in *in)
853
839
  in->buf = NULL;
854
840
  }
855
841
 
856
- /* Mark read cursor as rewind target. Ioless checkpoints at a packet
857
- * boundary so a mid-parse CHC_WOULD_BLOCK can rewind and re-parse once more bytes arrive. */
842
+ /* Mark read cursor for rewind. Ioless mode checkpoints at packet boundaries,
843
+ * allowing mid-parse CHC_WOULD_BLOCK to rewind & retry after more bytes arrive */
858
844
  CHC_MAYBE_UNUSED static void
859
845
  chc__in_checkpoint(chc_in *in)
860
846
  {
@@ -887,7 +873,7 @@ chc__in_refill(chc_in *in, chc_err *err)
887
873
  if (in->eof) return chc__err_set(err, CHC_ERR_EOF, "unexpected eof");
888
874
 
889
875
  if (in->io->check_cancel && in->io->check_cancel(in->io->ud))
890
- return chc__err_set(err, CHC_ERR_CANCELLED, "cancelled");
876
+ return chc__err_set(err, CHC_ERR_CANCELLED, "canceled");
891
877
 
892
878
  in->pos = 0;
893
879
  in->fill = 0;
@@ -933,13 +919,13 @@ chc__read_bytes(chc_in *in, void *dst, size_t n, chc_err *err)
933
919
  * straight into caller's dst to skip the staging memcpy. Only fires
934
920
  * after the staging buf is drained, so buffered-reader invariants
935
921
  * (pos, fill, consumed) stay consistent. Disabled in ioless: bypassed
936
- * bytes land outside in->buf and can't be rewound, so ioless routes
937
- * everything through the (growable) staging buf. */
922
+ * bytes bypass in->buf & cannot be rewound; ioless routes all reads
923
+ * through growable staging buf. */
938
924
  while (!CHC__IOLESS(in) && n > in->cap) {
939
925
  if (in->eof)
940
926
  return chc__err_set(err, CHC_ERR_EOF, "short read");
941
927
  if (in->io->check_cancel && in->io->check_cancel(in->io->ud))
942
- return chc__err_set(err, CHC_ERR_CANCELLED, "cancelled");
928
+ return chc__err_set(err, CHC_ERR_CANCELLED, "canceled");
943
929
  size_t got = 0;
944
930
  int rc = in->io->read(in->io->ud, p, n, &got, err);
945
931
  if (rc != CHC_OK) {
@@ -973,6 +959,19 @@ chc__read_bytes(chc_in *in, void *dst, size_t n, chc_err *err)
973
959
  return CHC_OK;
974
960
  }
975
961
 
962
+ static int
963
+ chc__skip_bytes(chc_in *in, size_t n, chc_err *err)
964
+ {
965
+ uint8_t sink[256];
966
+ while (n) {
967
+ size_t take = n < sizeof sink ? n : sizeof sink;
968
+ int rc = chc__read_bytes(in, sink, take, err);
969
+ if (rc != CHC_OK) return rc;
970
+ n -= take;
971
+ }
972
+ return CHC_OK;
973
+ }
974
+
976
975
  static int
977
976
  chc__read_varuint(chc_in *in, uint64_t *out, chc_err *err)
978
977
  {
@@ -1046,6 +1045,8 @@ struct chc_type {
1046
1045
  struct { int precision, scale; } decimal; /* Decimal(P, S) */
1047
1046
  struct { int scale; char *tz; size_t tz_len; } temporal; /* DateTime / DateTime64 / Time64 */
1048
1047
  struct { size_t dimension; } qbit; /* QBit(T, N): N; element type in children[0] */
1048
+ struct { char *func; size_t func_len; } agg;
1049
+ chc_interval_unit interval; /* IntervalNanosecond .. IntervalYear */
1049
1050
  struct {
1050
1051
  size_t n;
1051
1052
  struct { char *name; uint32_t name_len; int16_t value; } *items;
@@ -1059,6 +1060,8 @@ static bool chc__kind_is_enum(chc_kind k)
1059
1060
  { return k == CHC_ENUM8 || k == CHC_ENUM16; }
1060
1061
  static bool chc__kind_has_tz(chc_kind k)
1061
1062
  { return k == CHC_DATETIME || k == CHC_DATETIME64 || k == CHC_TIME64; }
1063
+ static bool chc__kind_is_agg(chc_kind k)
1064
+ { return k == CHC_AGGREGATE_FUNCTION || k == CHC_SIMPLE_AGGREGATE_FUNCTION; }
1062
1065
 
1063
1066
  void
1064
1067
  chc_type_destroy(chc_type *t, const chc_alloc *al)
@@ -1081,6 +1084,8 @@ chc_type_destroy(chc_type *t, const chc_alloc *al)
1081
1084
  t->enum_.n * sizeof *t->enum_.items);
1082
1085
  } else if (chc__kind_has_tz(t->kind))
1083
1086
  al->free(al->ud, t->temporal.tz, t->temporal.tz_len + 1);
1087
+ else if (chc__kind_is_agg(t->kind))
1088
+ al->free(al->ud, t->agg.func, t->agg.func_len + 1);
1084
1089
  al->free(al->ud, t->name, t->name_len + 1);
1085
1090
  al->free(al->ud, t, sizeof *t);
1086
1091
  }
@@ -1091,6 +1096,7 @@ const chc_type *chc_type_child(const chc_type *t, size_t i) { return (t && i
1091
1096
  int chc_type_fixed_size(const chc_type *t) { return t && t->kind == CHC_FIXED_STRING ? t->fixed_string.n : 0; }
1092
1097
  int chc_type_decimal_scale(const chc_type *t) { return (t && chc__kind_is_decimal(t->kind)) ? t->decimal.scale : 0; }
1093
1098
  int chc_type_datetime64_scale(const chc_type *t) { return (t && (t->kind == CHC_DATETIME64 || t->kind == CHC_TIME64)) ? t->temporal.scale : 0; }
1099
+ chc_interval_unit chc_type_interval_unit(const chc_type *t) { return (t && t->kind == CHC_INTERVAL) ? t->interval : CHC_INTERVAL_NONE; }
1094
1100
  size_t chc_type_qbit_dimension(const chc_type *t) { return (t && t->kind == CHC_QBIT) ? t->qbit.dimension : 0; }
1095
1101
  size_t chc_type_qbit_element_size(const chc_type *t) { return (t && t->kind == CHC_QBIT && t->n_children == 1) ? chc_type_elem_size(t->children[0]) * 8 : 0; }
1096
1102
  const char *chc_type_name(const chc_type *t, size_t *out_len) {
@@ -1120,7 +1126,8 @@ void chc_type_enum_at(const chc_type *t, size_t i,
1120
1126
  const char *
1121
1127
  chc_type_tuple_field_name(const chc_type *t, size_t i, size_t *out_len)
1122
1128
  {
1123
- if (!t || t->kind != CHC_TUPLE || !t->field_names || i >= t->n_children) {
1129
+ bool named = t && (t->kind == CHC_TUPLE || t->kind == CHC_NESTED);
1130
+ if (!named || !t->field_names || i >= t->n_children) {
1124
1131
  if (out_len) *out_len = 0;
1125
1132
  return NULL;
1126
1133
  }
@@ -1128,6 +1135,14 @@ chc_type_tuple_field_name(const chc_type *t, size_t i, size_t *out_len)
1128
1135
  return t->field_names[i];
1129
1136
  }
1130
1137
 
1138
+ const char *
1139
+ chc_type_agg_function(const chc_type *t, size_t *out_len)
1140
+ {
1141
+ bool has = t && chc__kind_is_agg(t->kind);
1142
+ if (out_len) *out_len = has ? t->agg.func_len : 0;
1143
+ return has ? t->agg.func : NULL;
1144
+ }
1145
+
1131
1146
  int
1132
1147
  chc_type_decimal_precision(const chc_type *t)
1133
1148
  {
@@ -1138,14 +1153,15 @@ chc_type_decimal_precision(const chc_type *t)
1138
1153
  case CHC_DECIMAL64: return 18;
1139
1154
  case CHC_DECIMAL128: return 38;
1140
1155
  case CHC_DECIMAL256: return 76;
1141
- default: return 0;
1156
+ default: CHC_UNREACHABLE();
1142
1157
  }
1143
1158
  }
1144
1159
 
1145
1160
  /* -------- type parser ---------- */
1146
1161
 
1147
- /* Tokens & lexer mirror clickhouse-cpp/types/type_parser.cpp. The parser
1148
- * is structurally identical (recursive on '(' / ')' / ','). */
1162
+ /* Tokens & lexer for printable ClickHouse type names. The parser is
1163
+ * recursive on '(' / ')' / ','; single-quoted parameter strings carry no
1164
+ * escapes. */
1149
1165
  typedef enum {
1150
1166
  CHC__TOK_EOS = 0, CHC__TOK_NAME, CHC__TOK_NUMBER, CHC__TOK_STRING,
1151
1167
  CHC__TOK_LPAREN, CHC__TOK_RPAREN, CHC__TOK_COMMA, CHC__TOK_EQ,
@@ -1180,8 +1196,8 @@ chc__next_tok(chc__lex *lx)
1180
1196
  if (c == ',') { lx->cur++; return (chc__tok){CHC__TOK_COMMA, st, 1, 0}; }
1181
1197
  if (c == '=') { lx->cur++; return (chc__tok){CHC__TOK_EQ, st, 1, 0}; }
1182
1198
  if (c == '\'') {
1183
- /* single-quoted string; clickhouse-cpp does not escape, so we
1184
- * accept anything up to the next unescaped quote. */
1199
+ /* single-quoted string; type names carry no escapes, so accept
1200
+ * anything up to the next quote. */
1185
1201
  lx->cur++;
1186
1202
  const char *body = lx->cur;
1187
1203
  while (lx->cur < lx->end && *lx->cur != '\'') lx->cur++;
@@ -1191,9 +1207,9 @@ chc__next_tok(chc__lex *lx)
1191
1207
  return (chc__tok){CHC__TOK_STRING, body, blen, 0};
1192
1208
  }
1193
1209
  if (c == '`' || c == '"') {
1194
- /* Quoted identifier, matching ClickHouse Lexer.cpp `quotedString`:
1195
- * doubled quote (`` `` `` or `""`) & backslash-escapes are skipped
1196
- * during scanning, resolved at copy time. */
1210
+ /* Quoted identifier: doubled quote (`` `` `` or `""`) &
1211
+ * backslash-escapes are skipped during scanning, resolved at
1212
+ * copy time -- same rule as the ClickHouse SQL lexer. */
1197
1213
  char q = c;
1198
1214
  lx->cur++;
1199
1215
  const char *body = lx->cur;
@@ -1272,91 +1288,133 @@ chc__atoi64(const char *s, size_t n, int64_t *out)
1272
1288
 
1273
1289
  /* AUTO-GENERATED-NAME-TABLE-BEGIN -- regenerate via tools/regen_name_table.sh */
1274
1290
  #define CHC__NAME_TABLE_M 256u
1275
- #define CHC__NAME_TABLE_SEED 720ull
1276
- struct chc__name_row { const char *name; chc_kind kind; };
1277
- static const struct chc__name_row chc__name_table[CHC__NAME_TABLE_M] = {
1278
- [ 4] = {"Int32", CHC_INT32},
1279
- [ 8] = {"Float32", CHC_FLOAT32},
1280
- [ 13] = {"MultiPolygon", CHC_MULTI_POLYGON},
1281
- [ 20] = {"DateTime", CHC_DATETIME},
1282
- [ 21] = {"Dynamic", CHC_DYNAMIC},
1283
- [ 30] = {"IntervalMinute", CHC_INTERVAL},
1284
- [ 33] = {"Ring", CHC_RING},
1285
- [ 36] = {"IntervalMicrosecond", CHC_INTERVAL},
1286
- [ 37] = {"Decimal64", CHC_DECIMAL64},
1287
- [ 40] = {"DateTime64", CHC_DATETIME64},
1288
- [ 43] = {"Int128", CHC_INT128},
1289
- [ 44] = {"Tuple", CHC_TUPLE},
1290
- [ 48] = {"IntervalDay", CHC_INTERVAL},
1291
- [ 49] = {"Map", CHC_MAP},
1292
- [ 50] = {"IntervalSecond", CHC_INTERVAL},
1293
- [ 52] = {"UInt8", CHC_UINT8},
1294
- [ 55] = {"Enum16", CHC_ENUM16},
1295
- [ 57] = {"IntervalMillisecond", CHC_INTERVAL},
1296
- [ 60] = {"Int8", CHC_INT8},
1297
- [ 65] = {"IntervalHour", CHC_INTERVAL},
1298
- [ 68] = {"UInt256", CHC_UINT256},
1299
- [ 73] = {"Date32", CHC_DATE32},
1300
- [ 74] = {"BFloat16", CHC_BFLOAT16},
1301
- [ 83] = {"Nullable", CHC_NULLABLE},
1302
- [ 89] = {"IntervalMonth", CHC_INTERVAL},
1303
- [101] = {"UInt128", CHC_UINT128},
1304
- [106] = {"Enum8", CHC_ENUM8},
1305
- [111] = {"Void", CHC_VOID},
1306
- [115] = {"IPv4", CHC_IPV4},
1307
- [120] = {"Variant", CHC_VARIANT},
1308
- [121] = {"LowCardinality", CHC_LOW_CARDINALITY},
1309
- [122] = {"Time64", CHC_TIME64},
1310
- [123] = {"Decimal128", CHC_DECIMAL128},
1311
- [130] = {"UInt64", CHC_UINT64},
1312
- [132] = {"UInt32", CHC_UINT32},
1313
- [133] = {"Int16", CHC_INT16},
1314
- [134] = {"JSON", CHC_JSON},
1315
- [135] = {"SimpleAggregateFunction", CHC_SIMPLE_AGGREGATE_FUNCTION},
1316
- [136] = {"IntervalNanosecond", CHC_INTERVAL},
1317
- [140] = {"QBit", CHC_QBIT},
1318
- [150] = {"Nothing", CHC_NOTHING},
1319
- [151] = {"Date", CHC_DATE},
1320
- [157] = {"IPv6", CHC_IPV6},
1321
- [168] = {"Array", CHC_ARRAY},
1322
- [172] = {"Time", CHC_TIME},
1323
- [177] = {"Object", CHC_OBJECT},
1324
- [178] = {"Decimal32", CHC_DECIMAL32},
1325
- [183] = {"Decimal256", CHC_DECIMAL256},
1326
- [189] = {"UUID", CHC_UUID},
1327
- [206] = {"Nested", CHC_NESTED},
1328
- [211] = {"Polygon", CHC_POLYGON},
1329
- [214] = {"String", CHC_STRING},
1330
- [218] = {"AggregateFunction", CHC_AGGREGATE_FUNCTION},
1331
- [219] = {"Int256", CHC_INT256},
1332
- [223] = {"UInt16", CHC_UINT16},
1333
- [224] = {"IntervalQuarter", CHC_INTERVAL},
1334
- [232] = {"Bool", CHC_BOOL},
1335
- [236] = {"FixedString", CHC_FIXED_STRING},
1336
- [237] = {"Int64", CHC_INT64},
1337
- [245] = {"IntervalYear", CHC_INTERVAL},
1338
- [246] = {"Float64", CHC_FLOAT64},
1339
- [253] = {"IntervalWeek", CHC_INTERVAL},
1340
- [254] = {"Point", CHC_POINT},
1291
+ #define CHC__NAME_TABLE_SEED 5935ull
1292
+ #define CHC__NAME_TABLE_MAXLEN 23u
1293
+ struct chc__name_row { uint16_t off; uint8_t len; uint8_t kind; uint8_t unit; };
1294
+ static const char chc__name_blob[] =
1295
+ "SimpleAggregateFunctionIntervalMicrosecondIntervalMillisecondInt"
1296
+ "ervalNanosecondMultiLineStringIntervalQuarterLowCardinalityInter"
1297
+ "valSecondIntervalMinuteIntervalMonthMultiPolygonIntervalHourInte"
1298
+ "rvalWeekIntervalYearFixedStringIntervalDayDateTime64Decimal128De"
1299
+ "cimal256Decimal32Decimal64BFloat16NullableUInt128UInt256Float32F"
1300
+ "loat64NothingVariantDynamicUInt16UInt32UInt64Date32Enum16NestedO"
1301
+ "bjectUInt8Enum8ArrayTuplePointBoolUUIDIPv4IPv6VoidRingJSONQBitMa"
1302
+ "p";
1303
+ static const struct chc__name_row chc__name_rows[] = {
1304
+ {390, 4, CHC_INT8, CHC_INTERVAL_NONE},
1305
+ {348, 5, CHC_INT16, CHC_INTERVAL_NONE},
1306
+ {354, 5, CHC_INT32, CHC_INTERVAL_NONE},
1307
+ {360, 5, CHC_INT64, CHC_INTERVAL_NONE},
1308
+ {299, 6, CHC_INT128, CHC_INTERVAL_NONE},
1309
+ {306, 6, CHC_INT256, CHC_INTERVAL_NONE},
1310
+ {389, 5, CHC_UINT8, CHC_INTERVAL_NONE},
1311
+ {347, 6, CHC_UINT16, CHC_INTERVAL_NONE},
1312
+ {353, 6, CHC_UINT32, CHC_INTERVAL_NONE},
1313
+ {359, 6, CHC_UINT64, CHC_INTERVAL_NONE},
1314
+ {298, 7, CHC_UINT128, CHC_INTERVAL_NONE},
1315
+ {305, 7, CHC_UINT256, CHC_INTERVAL_NONE},
1316
+ {312, 7, CHC_FLOAT32, CHC_INTERVAL_NONE},
1317
+ {319, 7, CHC_FLOAT64, CHC_INTERVAL_NONE},
1318
+ {282, 8, CHC_BFLOAT16, CHC_INTERVAL_NONE},
1319
+ {414, 4, CHC_BOOL, CHC_INTERVAL_NONE},
1320
+ { 88, 6, CHC_STRING, CHC_INTERVAL_NONE},
1321
+ {212, 11, CHC_FIXED_STRING, CHC_INTERVAL_NONE},
1322
+ {234, 4, CHC_DATE, CHC_INTERVAL_NONE},
1323
+ {365, 6, CHC_DATE32, CHC_INTERVAL_NONE},
1324
+ {234, 8, CHC_DATETIME, CHC_INTERVAL_NONE},
1325
+ {234, 10, CHC_DATETIME64, CHC_INTERVAL_NONE},
1326
+ {238, 4, CHC_TIME, CHC_INTERVAL_NONE},
1327
+ {238, 6, CHC_TIME64, CHC_INTERVAL_NONE},
1328
+ {418, 4, CHC_UUID, CHC_INTERVAL_NONE},
1329
+ {422, 4, CHC_IPV4, CHC_INTERVAL_NONE},
1330
+ {426, 4, CHC_IPV6, CHC_INTERVAL_NONE},
1331
+ {394, 5, CHC_ENUM8, CHC_INTERVAL_NONE},
1332
+ {371, 6, CHC_ENUM16, CHC_INTERVAL_NONE},
1333
+ {264, 9, CHC_DECIMAL32, CHC_INTERVAL_NONE},
1334
+ {273, 9, CHC_DECIMAL64, CHC_INTERVAL_NONE},
1335
+ {244, 10, CHC_DECIMAL128, CHC_INTERVAL_NONE},
1336
+ {254, 10, CHC_DECIMAL256, CHC_INTERVAL_NONE},
1337
+ {290, 8, CHC_NULLABLE, CHC_INTERVAL_NONE},
1338
+ {399, 5, CHC_ARRAY, CHC_INTERVAL_NONE},
1339
+ {404, 5, CHC_TUPLE, CHC_INTERVAL_NONE},
1340
+ {446, 3, CHC_MAP, CHC_INTERVAL_NONE},
1341
+ {377, 6, CHC_NESTED, CHC_INTERVAL_NONE},
1342
+ {109, 14, CHC_LOW_CARDINALITY, CHC_INTERVAL_NONE},
1343
+ {326, 7, CHC_NOTHING, CHC_INTERVAL_NONE},
1344
+ {430, 4, CHC_VOID, CHC_INTERVAL_NONE},
1345
+ {409, 5, CHC_POINT, CHC_INTERVAL_NONE},
1346
+ {434, 4, CHC_RING, CHC_INTERVAL_NONE},
1347
+ {169, 7, CHC_POLYGON, CHC_INTERVAL_NONE},
1348
+ {164, 12, CHC_MULTI_POLYGON, CHC_INTERVAL_NONE},
1349
+ { 84, 10, CHC_LINE_STRING, CHC_INTERVAL_NONE},
1350
+ { 79, 15, CHC_MULTI_LINE_STRING, CHC_INTERVAL_NONE},
1351
+ { 0, 23, CHC_SIMPLE_AGGREGATE_FUNCTION, CHC_INTERVAL_NONE},
1352
+ { 6, 17, CHC_AGGREGATE_FUNCTION, CHC_INTERVAL_NONE},
1353
+ {333, 7, CHC_VARIANT, CHC_INTERVAL_NONE},
1354
+ {340, 7, CHC_DYNAMIC, CHC_INTERVAL_NONE},
1355
+ {438, 4, CHC_JSON, CHC_INTERVAL_NONE},
1356
+ {383, 6, CHC_OBJECT, CHC_INTERVAL_NONE},
1357
+ {442, 4, CHC_QBIT, CHC_INTERVAL_NONE},
1358
+ { 61, 18, CHC_INTERVAL, CHC_INTERVAL_NANOSECOND},
1359
+ { 23, 19, CHC_INTERVAL, CHC_INTERVAL_MICROSECOND},
1360
+ { 42, 19, CHC_INTERVAL, CHC_INTERVAL_MILLISECOND},
1361
+ {123, 14, CHC_INTERVAL, CHC_INTERVAL_SECOND},
1362
+ {137, 14, CHC_INTERVAL, CHC_INTERVAL_MINUTE},
1363
+ {176, 12, CHC_INTERVAL, CHC_INTERVAL_HOUR},
1364
+ {223, 11, CHC_INTERVAL, CHC_INTERVAL_DAY},
1365
+ {188, 12, CHC_INTERVAL, CHC_INTERVAL_WEEK},
1366
+ {151, 13, CHC_INTERVAL, CHC_INTERVAL_MONTH},
1367
+ { 94, 15, CHC_INTERVAL, CHC_INTERVAL_QUARTER},
1368
+ {200, 12, CHC_INTERVAL, CHC_INTERVAL_YEAR},
1369
+ };
1370
+ static const uint8_t chc__name_slot[CHC__NAME_TABLE_M] = {
1371
+ [ 2] = 43, [ 6] = 36, [ 9] = 60, [ 27] = 7, [ 29] = 40, [ 36] = 53,
1372
+ [ 39] = 41, [ 42] = 65, [ 45] = 34, [ 51] = 28, [ 52] = 46, [ 59] = 16,
1373
+ [ 67] = 18, [ 68] = 29, [ 71] = 2, [ 72] = 61, [ 73] = 25, [ 80] = 49,
1374
+ [ 81] = 22, [ 85] = 45, [ 86] = 64, [ 97] = 31, [ 98] = 35, [107] = 24,
1375
+ [109] = 39, [111] = 54, [112] = 55, [113] = 4, [118] = 9, [123] = 57,
1376
+ [126] = 47, [127] = 11, [129] = 21, [133] = 59, [134] = 19, [137] = 58,
1377
+ [138] = 17, [139] = 20, [141] = 27, [142] = 42, [151] = 63, [153] = 23,
1378
+ [154] = 13, [159] = 1, [160] = 15, [168] = 50, [170] = 33, [173] = 48,
1379
+ [174] = 37, [175] = 30, [176] = 62, [180] = 52, [192] = 14, [202] = 5,
1380
+ [204] = 6, [205] = 38, [206] = 3, [215] = 44, [223] = 12, [226] = 8,
1381
+ [238] = 10, [240] = 56, [247] = 26, [251] = 51, [252] = 32,
1341
1382
  };
1342
1383
  /* AUTO-GENERATED-NAME-TABLE-END */
1343
1384
 
1344
1385
  /* Plain "Decimal" is intentionally absent from the table; the parser's
1345
- * decimal_alias branch resolves it from precision. Miss -> CHC_VOID, also
1346
- * the sentinel for unknown names; caller disambiguates with an explicit
1347
- * memcmp against "Void". */
1348
- static chc_kind
1349
- chc__name_to_kind(const char *s, size_t n) CHC_REPRODUCIBLE
1386
+ * decimal_alias branch resolves it from precision. */
1387
+ static const struct chc__name_row *
1388
+ chc__name_lookup(const char *s, size_t n) CHC_REPRODUCIBLE
1350
1389
  {
1351
- if (n == 0 || n > 23) return CHC_VOID;
1390
+ if (n == 0 || n > CHC__NAME_TABLE_MAXLEN) return NULL;
1352
1391
  size_t h_len = n < 16 ? n : 16;
1353
1392
  uint64_t h = chc__city_hash_len_16(
1354
1393
  chc__city_hash_len_0_to_16(s, h_len) + (uint64_t) n,
1355
1394
  CHC__NAME_TABLE_SEED);
1356
- const struct chc__name_row *r = &chc__name_table[h & (CHC__NAME_TABLE_M - 1)];
1357
- if (r->name && strlen(r->name) == n && memcmp(r->name, s, n) == 0)
1358
- return r->kind;
1359
- return CHC_VOID;
1395
+ uint8_t i = chc__name_slot[h & (CHC__NAME_TABLE_M - 1)];
1396
+ if (!i) return NULL;
1397
+ const struct chc__name_row *r = &chc__name_rows[i - 1];
1398
+ return (r->len == n && memcmp(chc__name_blob + r->off, s, n) == 0) ? r : NULL;
1399
+ }
1400
+
1401
+ /* Skip aggregate and JSON parameters, which can contain more than type names */
1402
+ static bool
1403
+ chc__skip_params(chc__lex *lx)
1404
+ {
1405
+ size_t depth = 1;
1406
+ while (lx->cur < lx->end) {
1407
+ char c = *lx->cur++;
1408
+ if (c == '\'' || c == '`' || c == '"') {
1409
+ while (lx->cur < lx->end && *lx->cur != c) lx->cur++;
1410
+ if (lx->cur == lx->end) return false;
1411
+ lx->cur++;
1412
+ } else if (c == '(')
1413
+ depth++;
1414
+ else if (c == ')' && !--depth)
1415
+ return true;
1416
+ }
1417
+ return false;
1360
1418
  }
1361
1419
 
1362
1420
  static int chc__parse_type(chc__lex *lx, const chc_alloc *al,
@@ -1383,20 +1441,58 @@ chc__type_push_enum(const chc_alloc *al, chc_type *parent,
1383
1441
  const char *name, size_t name_len, int64_t value,
1384
1442
  chc_err *err)
1385
1443
  {
1444
+ char *nm = chc__strdup(al, name, name_len, err);
1445
+ if (!nm) return CHC_ERR_OOM;
1386
1446
  size_t n = parent->enum_.n;
1387
1447
  void *arr = chc__realloc(al, parent->enum_.items,
1388
1448
  n * sizeof *parent->enum_.items,
1389
1449
  (n + 1) * sizeof *parent->enum_.items, err);
1390
- if (!arr) return CHC_ERR_OOM;
1450
+ if (!arr) { al->free(al->ud, nm, name_len + 1); return CHC_ERR_OOM; }
1391
1451
  parent->enum_.items = arr;
1392
- parent->enum_.items[n].name = chc__strdup(al, name, name_len, err);
1393
- if (!parent->enum_.items[n].name) return CHC_ERR_OOM;
1452
+ parent->enum_.items[n].name = nm;
1394
1453
  parent->enum_.items[n].name_len = name_len;
1395
1454
  parent->enum_.items[n].value = value;
1396
1455
  parent->enum_.n = n + 1;
1397
1456
  return CHC_OK;
1398
1457
  }
1399
1458
 
1459
+ /* Tuple field-name scratch: parallel arrays handed to the type on success,
1460
+ * freed exactly as far as each grew when a mid-list allocation fails. */
1461
+ typedef struct {
1462
+ char **names;
1463
+ size_t *lens;
1464
+ size_t n_names;
1465
+ size_t n_lens;
1466
+ } chc__fields;
1467
+
1468
+ static void
1469
+ chc__fields_free(const chc_alloc *al, chc__fields *f)
1470
+ {
1471
+ for (size_t i = 0; i < f->n_names && i < f->n_lens; i++)
1472
+ al->free(al->ud, f->names[i], f->lens[i] + 1);
1473
+ al->free(al->ud, f->names, f->n_names * sizeof *f->names);
1474
+ al->free(al->ud, f->lens, f->n_lens * sizeof *f->lens);
1475
+ *f = (chc__fields) {};
1476
+ }
1477
+
1478
+ static int
1479
+ chc__fields_grow(const chc_alloc *al, chc__fields *f, size_t n, chc_err *err)
1480
+ {
1481
+ char **nn = chc__realloc(al, f->names, f->n_names * sizeof *f->names,
1482
+ n * sizeof *f->names, err);
1483
+ if (!nn) return CHC_ERR_OOM;
1484
+ f->names = nn;
1485
+ f->names[n - 1] = NULL;
1486
+ f->n_names = n;
1487
+ size_t *nl = chc__realloc(al, f->lens, f->n_lens * sizeof *f->lens,
1488
+ n * sizeof *f->lens, err);
1489
+ if (!nl) return CHC_ERR_OOM;
1490
+ f->lens = nl;
1491
+ f->lens[n - 1] = 0;
1492
+ f->n_lens = n;
1493
+ return CHC_OK;
1494
+ }
1495
+
1400
1496
  static int
1401
1497
  chc__parse_type(chc__lex *lx, const chc_alloc *al,
1402
1498
  const char *whole_start, const char *whole_end,
@@ -1417,17 +1513,23 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1417
1513
  if (decimal_alias) {
1418
1514
  t->kind = CHC_DECIMAL128; /* placeholder; refined from precision */
1419
1515
  } else {
1420
- t->kind = chc__name_to_kind(head.start, head.len);
1421
- if (t->kind == CHC_VOID && !(head.len == 4 && memcmp(head.start, "Void", 4) == 0)) {
1516
+ const struct chc__name_row *row = chc__name_lookup(head.start, head.len);
1517
+ if (!row) {
1422
1518
  chc_type_destroy(t, al);
1423
1519
  return chc__err_set(err, CHC_ERR_TYPE, "unknown type: %.*s",
1424
1520
  (int) head.len, head.start);
1425
1521
  }
1522
+ t->kind = (chc_kind) row->kind;
1523
+ if (t->kind == CHC_INTERVAL) t->interval = (chc_interval_unit) row->unit;
1426
1524
  }
1427
1525
 
1428
1526
  const char *name_start = head.start;
1429
1527
  const char *name_end = head.start + head.len;
1430
1528
 
1529
+ /* ClickHouse defaults to milliseconds. */
1530
+ if (t->kind == CHC_DATETIME64 || t->kind == CHC_TIME64)
1531
+ t->temporal.scale = 3;
1532
+
1431
1533
  /* Optional parameter list. */
1432
1534
  if (chc__peek_tok(lx).kind == CHC__TOK_LPAREN) {
1433
1535
  chc__eat_tok(lx);
@@ -1526,20 +1628,22 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1526
1628
  }
1527
1629
  t->decimal.scale = (int) scale;
1528
1630
  } else if (t->kind == CHC_DATETIME64 || t->kind == CHC_TIME64) {
1529
- chc__tok num = chc__eat_tok(lx);
1530
- if (num.kind != CHC__TOK_NUMBER) {
1531
- chc_type_destroy(t, al);
1532
- return chc__err_set(err, CHC_ERR_TYPE, "DateTime64: expected precision");
1533
- }
1534
- int64_t scale;
1535
- if (!chc__atoi64(num.start, num.len, &scale)
1536
- || scale < 0 || scale > 9) {
1537
- chc_type_destroy(t, al);
1538
- return chc__err_set(err, CHC_ERR_TYPE,
1539
- "DateTime64: precision out of range: %.*s",
1540
- (int) num.len, num.start);
1631
+ if (chc__peek_tok(lx).kind != CHC__TOK_RPAREN) {
1632
+ chc__tok num = chc__eat_tok(lx);
1633
+ if (num.kind != CHC__TOK_NUMBER) {
1634
+ chc_type_destroy(t, al);
1635
+ return chc__err_set(err, CHC_ERR_TYPE, "DateTime64: expected precision");
1636
+ }
1637
+ int64_t scale;
1638
+ if (!chc__atoi64(num.start, num.len, &scale)
1639
+ || scale < 0 || scale > 9) {
1640
+ chc_type_destroy(t, al);
1641
+ return chc__err_set(err, CHC_ERR_TYPE,
1642
+ "DateTime64: precision out of range: %.*s",
1643
+ (int) num.len, num.start);
1644
+ }
1645
+ t->temporal.scale = (int) scale;
1541
1646
  }
1542
- t->temporal.scale = (int) scale;
1543
1647
  if (chc__peek_tok(lx).kind == CHC__TOK_COMMA) {
1544
1648
  chc__eat_tok(lx);
1545
1649
  chc__tok s = chc__eat_tok(lx);
@@ -1560,12 +1664,19 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1560
1664
  t->temporal.tz = chc__strdup(al, s.start, s.len, err);
1561
1665
  if (!t->temporal.tz) { chc_type_destroy(t, al); return CHC_ERR_OOM; }
1562
1666
  t->temporal.tz_len = s.len;
1667
+ } else if (t->kind == CHC_JSON) {
1668
+ /* JSON options affect server storage but not JSON sent as strings */
1669
+ if (!chc__skip_params(lx)) {
1670
+ chc_type_destroy(t, al);
1671
+ return chc__err_set(err, CHC_ERR_TYPE,
1672
+ "JSON: unterminated parameters");
1673
+ }
1674
+ lx->cur--;
1563
1675
  } else if (t->kind == CHC_OBJECT) {
1564
- /* Object('name') -- legacy JSON object syntax. Argument is a
1565
- * schema identifier (eg 'json'); clickhouse-cpp accepts any
1566
- * quoted string. Wire format matches CHC_JSON, so we discard
1567
- * the argument and keep the full source text in t->name for
1568
- * round-trip & error messages. */
1676
+ /* Object('name'), legacy JSON object syntax. Argument is schema
1677
+ * identifier (eg 'json'); any quoted string is accepted.
1678
+ * Discard argument & retain full source text in t->name
1679
+ * for round-trip & errors */
1569
1680
  chc__tok s = chc__eat_tok(lx);
1570
1681
  if (s.kind != CHC__TOK_STRING) {
1571
1682
  chc_type_destroy(t, al);
@@ -1605,23 +1716,66 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1605
1716
  (int) num.len, num.start);
1606
1717
  }
1607
1718
  t->qbit.dimension = (size_t) n;
1719
+ } else if (chc__kind_is_agg(t->kind)) {
1720
+ /* ClickHouse may include a version before an aggregate function name */
1721
+ if (chc__peek_tok(lx).kind == CHC__TOK_NUMBER) {
1722
+ chc__eat_tok(lx);
1723
+ if (chc__eat_tok(lx).kind != CHC__TOK_COMMA) {
1724
+ chc_type_destroy(t, al);
1725
+ return chc__err_set(err, CHC_ERR_TYPE,
1726
+ "%.*s: expected ',' after version",
1727
+ (int) head.len, head.start);
1728
+ }
1729
+ }
1730
+ chc__tok fn = chc__eat_tok(lx);
1731
+ if (fn.kind != CHC__TOK_NAME || fn.quote) {
1732
+ chc_type_destroy(t, al);
1733
+ return chc__err_set(err, CHC_ERR_TYPE,
1734
+ "%.*s: expected function name", (int) head.len, head.start);
1735
+ }
1736
+ t->agg.func = chc__strdup(al, fn.start, fn.len, err);
1737
+ if (!t->agg.func) { chc_type_destroy(t, al); return CHC_ERR_OOM; }
1738
+ t->agg.func_len = fn.len;
1739
+ if (chc__peek_tok(lx).kind == CHC__TOK_LPAREN) {
1740
+ chc__eat_tok(lx);
1741
+ if (!chc__skip_params(lx)) {
1742
+ chc_type_destroy(t, al);
1743
+ return chc__err_set(err, CHC_ERR_TYPE,
1744
+ "%.*s: %.*s parameters unterminated",
1745
+ (int) head.len, head.start, (int) fn.len, fn.start);
1746
+ }
1747
+ }
1748
+ while (chc__peek_tok(lx).kind == CHC__TOK_COMMA) {
1749
+ chc__eat_tok(lx);
1750
+ chc_type *arg = NULL;
1751
+ int rc = chc__parse_type(lx, al, whole_start, whole_end,
1752
+ depth + 1, &arg, err);
1753
+ if (rc == CHC_OK) rc = chc__type_push_child(al, t, arg, err);
1754
+ if (rc != CHC_OK) {
1755
+ chc_type_destroy(arg, al);
1756
+ chc_type_destroy(t, al);
1757
+ return rc;
1758
+ }
1759
+ }
1760
+ if (!t->n_children) {
1761
+ chc_type_destroy(t, al);
1762
+ return chc__err_set(err, CHC_ERR_TYPE,
1763
+ "%.*s: %.*s has no argument type",
1764
+ (int) head.len, head.start, (int) fn.len, fn.start);
1765
+ }
1608
1766
  } else {
1609
- /* Generic composite: comma-separated type list. Tuple children
1610
- * may carry an optional leading NAME (field label) before the
1611
- * type. Field names are stored in a parallel array on the
1612
- * parent. */
1613
- bool is_tuple = (t->kind == CHC_TUPLE);
1614
- char **fn_buf = NULL;
1615
- size_t *fn_lens = NULL;
1616
- size_t fn_cap = 0;
1617
- bool any_named = false;
1767
+ /* Tuple and Nested fields may have names before their types */
1768
+ bool named = (t->kind == CHC_TUPLE
1769
+ || t->kind == CHC_NESTED);
1770
+ bool any_named = false;
1771
+ chc__fields fields = {};
1618
1772
  for (;;) {
1619
1773
  chc__tok la = chc__peek_tok(lx);
1620
1774
  if (la.kind == CHC__TOK_RPAREN) break;
1621
1775
 
1622
1776
  chc__tok field = {};
1623
1777
  bool has_field = false;
1624
- if (is_tuple && la.kind == CHC__TOK_NAME) {
1778
+ if (named && la.kind == CHC__TOK_NAME) {
1625
1779
  chc__eat_tok(lx);
1626
1780
  if (la.quote) {
1627
1781
  /* `\`x\`` or `"x"` is never a type head, so it must be
@@ -1648,80 +1802,45 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1648
1802
 
1649
1803
  chc_type *child = NULL;
1650
1804
  int rc = chc__parse_type(lx, al, whole_start, whole_end, depth + 1, &child, err);
1651
- if (rc == CHC_OK)
1805
+ if (rc == CHC_OK) {
1652
1806
  rc = chc__type_push_child(al, t, child, err);
1653
- else
1807
+ if (rc == CHC_OK) child = NULL; /* t owns it now */
1808
+ } else
1654
1809
  child = NULL;
1810
+ if (rc == CHC_OK && named)
1811
+ rc = chc__fields_grow(al, &fields, t->n_children, err);
1812
+ if (rc == CHC_OK && named && has_field) {
1813
+ size_t flen = field.len;
1814
+ char *nm = field.quote
1815
+ ? chc__strdup_unquote(al, field.start, field.len,
1816
+ field.quote, &flen, err)
1817
+ : chc__strdup(al, field.start, field.len, err);
1818
+ if (nm) {
1819
+ fields.names[fields.n_names - 1] = nm;
1820
+ fields.lens[fields.n_names - 1] = flen;
1821
+ any_named = true;
1822
+ } else
1823
+ rc = CHC_ERR_OOM;
1824
+ }
1655
1825
  if (rc != CHC_OK) {
1656
1826
  if (child) chc_type_destroy(child, al);
1657
- if (fn_buf) {
1658
- for (size_t i = 0; i < fn_cap; i++)
1659
- al->free(al->ud, fn_buf[i], fn_lens[i] + 1);
1660
- al->free(al->ud, fn_buf, fn_cap * sizeof *fn_buf);
1661
- al->free(al->ud, fn_lens, fn_cap * sizeof *fn_lens);
1662
- }
1827
+ chc__fields_free(al, &fields);
1663
1828
  chc_type_destroy(t, al);
1664
1829
  return rc;
1665
1830
  }
1666
1831
 
1667
- if (is_tuple) {
1668
- size_t new_cap = t->n_children;
1669
- char **nfn = chc__realloc(al, fn_buf,
1670
- fn_cap * sizeof *fn_buf,
1671
- new_cap * sizeof *fn_buf, err);
1672
- if (!nfn) { chc_type_destroy(t, al); return CHC_ERR_OOM; }
1673
- size_t *nfl = chc__realloc(al, fn_lens,
1674
- fn_cap * sizeof *fn_lens,
1675
- new_cap * sizeof *fn_lens, err);
1676
- if (!nfl) {
1677
- al->free(al->ud, nfn, new_cap * sizeof *nfn);
1678
- chc_type_destroy(t, al); return CHC_ERR_OOM;
1679
- }
1680
- fn_buf = nfn;
1681
- fn_lens = nfl;
1682
- fn_buf[fn_cap] = NULL;
1683
- fn_lens[fn_cap] = 0;
1684
- fn_cap = new_cap;
1685
- if (has_field) {
1686
- size_t flen = field.len;
1687
- if (field.quote)
1688
- fn_buf[fn_cap - 1] = chc__strdup_unquote(al, field.start,
1689
- field.len, field.quote,
1690
- &flen, err);
1691
- else
1692
- fn_buf[fn_cap - 1] = chc__strdup(al, field.start,
1693
- field.len, err);
1694
- if (!fn_buf[fn_cap - 1]) {
1695
- for (size_t i = 0; i < fn_cap - 1; i++)
1696
- al->free(al->ud, fn_buf[i], fn_lens[i] + 1);
1697
- al->free(al->ud, fn_buf, fn_cap * sizeof *fn_buf);
1698
- al->free(al->ud, fn_lens, fn_cap * sizeof *fn_lens);
1699
- chc_type_destroy(t, al); return CHC_ERR_OOM;
1700
- }
1701
- fn_lens[fn_cap - 1] = flen;
1702
- any_named = true;
1703
- }
1704
- }
1705
-
1706
1832
  chc__tok c = chc__peek_tok(lx);
1707
1833
  if (c.kind == CHC__TOK_COMMA) { chc__eat_tok(lx); continue; }
1708
1834
  if (c.kind == CHC__TOK_RPAREN) break;
1709
- if (fn_buf) {
1710
- for (size_t i = 0; i < fn_cap; i++)
1711
- al->free(al->ud, fn_buf[i], fn_lens[i] + 1);
1712
- al->free(al->ud, fn_buf, fn_cap * sizeof *fn_buf);
1713
- al->free(al->ud, fn_lens, fn_cap * sizeof *fn_lens);
1714
- }
1835
+ chc__fields_free(al, &fields);
1715
1836
  chc_type_destroy(t, al);
1716
1837
  return chc__err_set(err, CHC_ERR_TYPE, "expected ',' or ')'");
1717
1838
  }
1718
1839
  if (any_named) {
1719
- t->field_names = fn_buf;
1720
- t->field_name_lens = fn_lens;
1721
- } else {
1722
- al->free(al->ud, fn_buf, fn_cap * sizeof *fn_buf);
1723
- al->free(al->ud, fn_lens, fn_cap * sizeof *fn_lens);
1724
- }
1840
+ t->field_names = fields.names;
1841
+ t->field_name_lens = fields.lens;
1842
+ } else
1843
+ chc__fields_free(al, &fields);
1725
1844
  }
1726
1845
 
1727
1846
  chc__tok rp = chc__eat_tok(lx);
@@ -1735,7 +1854,7 @@ chc__parse_type(chc__lex *lx, const chc_alloc *al,
1735
1854
  /* Decimal(P, S) compatibility: width selected by precision. */
1736
1855
  if (t->kind == CHC_DECIMAL128 && head.len == 7
1737
1856
  && memcmp(head.start, "Decimal", 7) == 0 && t->n_children == 0) {
1738
- /* unparenthesised "Decimal" without (P, S) — treat as Decimal128 */
1857
+ /* Treat Decimal without parameters as Decimal128 */
1739
1858
  }
1740
1859
 
1741
1860
  t->name = chc__strdup(al, name_start, (size_t) (name_end - name_start), err);
@@ -1778,19 +1897,7 @@ chc_type_format(const chc_type *t, char *buf, size_t buf_len)
1778
1897
  }
1779
1898
 
1780
1899
  /* -------- column internals ---------- */
1781
-
1782
- struct chc_column {
1783
- chc_col_kind layout;
1784
- size_t n_rows;
1785
- union {
1786
- struct { void *data; size_t elem_size; } fixed;
1787
- struct { uint8_t *data; uint64_t *offsets; size_t bytes; } str;
1788
- struct { uint8_t *null_map; chc_column *inner; } nullable;
1789
- struct { uint64_t *offsets; chc_column *values; } array;
1790
- struct { chc_column **children; size_t arity; } tuple;
1791
- struct { int key_size; void *keys; chc_column *dict; size_t dict_n; } lc;
1792
- };
1793
- };
1900
+ /* Public API defines struct chc_column */
1794
1901
 
1795
1902
  chc_col_kind chc_column_layout(const chc_column *c) { return c ? c->layout : (chc_col_kind) 0; }
1796
1903
  size_t chc_column_n_rows(const chc_column *c) { return c ? c->n_rows : 0; }
@@ -2019,8 +2126,10 @@ chc__col_read_string(chc_in *in, size_t n_rows,
2019
2126
  return CHC_OK;
2020
2127
  }
2021
2128
 
2022
- /* Composite columns might have a prefix sub-stream. Only LowCardinality
2023
- * actually emits one in the formats we handle: a uint64 key version. */
2129
+ /* Composite columns might have a prefix sub-stream, emitted for whole
2130
+ * column tree before any body bytes. In formats handled here:
2131
+ * LowCardinality has a uint64 key version, JSON a uint64 serialization
2132
+ * version, legacy Object a uint8 serialization kind */
2024
2133
  static int
2025
2134
  chc__col_read_prefix(chc_in *in, const chc_type *t, chc_err *err)
2026
2135
  {
@@ -2033,8 +2142,34 @@ chc__col_read_prefix(chc_in *in, const chc_type *t, chc_err *err)
2033
2142
  "LowCardinality: unexpected key version %llu", (unsigned long long) v);
2034
2143
  return CHC_OK;
2035
2144
  }
2145
+ if (t->kind == CHC_JSON) {
2146
+ /* Only STRING (=1) in scope; other versions need the consumer to
2147
+ * set output_format_native_write_json_as_string=1 on the SELECT */
2148
+ uint64_t v;
2149
+ int rc = chc__read_u64_le(in, &v, err);
2150
+ if (rc != CHC_OK) return rc;
2151
+ if (v != 1)
2152
+ return chc__err_set(err, CHC_ERR_TYPE,
2153
+ "unsupported JSON serialization version %llu "
2154
+ "(set output_format_native_write_json_as_string=1)",
2155
+ (unsigned long long) v);
2156
+ return CHC_OK;
2157
+ }
2158
+ if (t->kind == CHC_OBJECT) {
2159
+ /* Legacy Object Native format uses a one-byte kind: TUPLE=0,
2160
+ * STRING=1. Tuple decoding needs its dynamic type descriptor */
2161
+ uint8_t kind;
2162
+ int rc = chc__read_byte(in, &kind, err);
2163
+ if (rc != CHC_OK) return rc;
2164
+ if (kind != 1)
2165
+ return chc__err_set(err, CHC_ERR_TYPE,
2166
+ "unsupported Object serialization kind %u (STRING=1 required)",
2167
+ (unsigned) kind);
2168
+ return CHC_OK;
2169
+ }
2036
2170
  if (t->kind == CHC_NULLABLE || t->kind == CHC_ARRAY
2037
2171
  || t->kind == CHC_TUPLE || t->kind == CHC_MAP
2172
+ || t->kind == CHC_NESTED
2038
2173
  || t->kind == CHC_SIMPLE_AGGREGATE_FUNCTION) {
2039
2174
  for (size_t i = 0; i < t->n_children; i++) {
2040
2175
  int rc = chc__col_read_prefix(in, t->children[i], err);
@@ -2044,12 +2179,65 @@ chc__col_read_prefix(chc_in *in, const chc_type *t, chc_err *err)
2044
2179
  return CHC_OK;
2045
2180
  }
2046
2181
 
2047
- /* Geo types are aliases for nested Array(...(Tuple(Float64,Float64))). depth
2048
- * 0 = Point, 1 = Ring (Array(Point)), 2 = Polygon (Array(Ring)),
2049
- * 3 = MultiPolygon (Array(Polygon)). Defined ahead of chc__col_read so it
2050
- * can call back into here. */
2051
- static int chc__col_read_geo(chc_in *in, int depth, size_t n_rows,
2052
- chc_column **out, chc_err *err);
2182
+ /* Geo types alias nested Arrays over Point = Tuple(Float64, Float64),
2183
+ * as in the server's type factory. Never mutated */
2184
+ static chc_type chc__geo_f64 = { .kind = CHC_FLOAT64, .name = (char *) "Float64", .name_len = 7 };
2185
+ static chc_type *chc__geo_point_elems[] = { &chc__geo_f64, &chc__geo_f64 };
2186
+ static chc_type chc__geo_point = {
2187
+ .kind = CHC_TUPLE, .name = (char *) "Point", .name_len = 5,
2188
+ .n_children = 2, .children = chc__geo_point_elems,
2189
+ };
2190
+ static chc_type *chc__geo_ring_elems[] = { &chc__geo_point };
2191
+ static chc_type chc__geo_ring = {
2192
+ .kind = CHC_ARRAY, .name = (char *) "Array(Point)", .name_len = 12,
2193
+ .n_children = 1, .children = chc__geo_ring_elems,
2194
+ };
2195
+ static chc_type *chc__geo_polygon_elems[] = { &chc__geo_ring };
2196
+ static chc_type chc__geo_polygon = {
2197
+ .kind = CHC_ARRAY, .name = (char *) "Array(Array(Point))", .name_len = 19,
2198
+ .n_children = 1, .children = chc__geo_polygon_elems,
2199
+ };
2200
+ static chc_type *chc__geo_multi_polygon_elems[] = { &chc__geo_polygon };
2201
+ static chc_type chc__geo_multi_polygon = {
2202
+ .kind = CHC_ARRAY, .name = (char *) "Array(Array(Array(Point)))", .name_len = 26,
2203
+ .n_children = 1, .children = chc__geo_multi_polygon_elems,
2204
+ };
2205
+
2206
+ static const chc_type *
2207
+ chc__geo_alias(const chc_type *t)
2208
+ {
2209
+ switch (t->kind) {
2210
+ case CHC_POINT: return &chc__geo_point;
2211
+ case CHC_RING:
2212
+ case CHC_LINE_STRING: return &chc__geo_ring;
2213
+ case CHC_POLYGON:
2214
+ case CHC_MULTI_LINE_STRING: return &chc__geo_polygon;
2215
+ case CHC_MULTI_POLYGON: return &chc__geo_multi_polygon;
2216
+ default: return t;
2217
+ }
2218
+ }
2219
+
2220
+ /* Resolve types serialized as another type, SimpleAggregateFunction stores
2221
+ * values using first argument type */
2222
+ static const chc_type *
2223
+ chc__serial_type(const chc_type *t)
2224
+ {
2225
+ while (t->kind == CHC_SIMPLE_AGGREGATE_FUNCTION && t->n_children)
2226
+ t = t->children[0];
2227
+ return chc__geo_alias(t);
2228
+ }
2229
+
2230
+ /* Serialization kind, DEFAULT is 0. 23.3 ISerialization::Kind & current
2231
+ * KindStackBinarySerializationType agree on both */
2232
+ #define CHC__KIND_SPARSE 1u
2233
+
2234
+ /* Final sparse offset group carries this flag (Native Format spec,
2235
+ * kind_stack & sparse encoding). */
2236
+ #define CHC__SPARSE_END (UINT64_C(1) << 62)
2237
+
2238
+ static int chc__col_read_kinds(chc_in *in, const chc_type *t, size_t n_rows,
2239
+ const uint8_t **kinds, chc_column **out,
2240
+ chc_err *err);
2053
2241
 
2054
2242
  /* Byte-swap a host-typed uint64/keys array in place on BE hosts. No-op on LE. */
2055
2243
  static void
@@ -2074,6 +2262,143 @@ chc__swap_keys(CHC_MAYBE_UNUSED void *p, CHC_MAYBE_UNUSED size_t n,
2074
2262
  #endif
2075
2263
  }
2076
2264
 
2265
+ /* Read t's children as Tuple column, serving Tuple, Map & Nested values.
2266
+ * kinds NULL when dense */
2267
+ static int
2268
+ chc__col_read_tuple(chc_in *in, const chc_type *t, size_t n_rows,
2269
+ const uint8_t **kinds, chc_column **out, chc_err *err)
2270
+ {
2271
+ const chc_alloc *al = in->al;
2272
+ chc_column *c = chc__calloc(al, sizeof *c, err);
2273
+ if (!c) return CHC_ERR_OOM;
2274
+ c->layout = CHC_COL_TUPLE;
2275
+ c->n_rows = n_rows;
2276
+ /* LOCAL PATCH (ch_connect): ClickHouse serializes the empty Tuple()
2277
+ * as one UInt8 (zero) per row; skipping nothing desyncs the stream. */
2278
+ if (t->n_children == 0) {
2279
+ uint8_t scratch[256];
2280
+ size_t left = n_rows;
2281
+ while (left) {
2282
+ size_t take = left > sizeof scratch ? sizeof scratch : left;
2283
+ int rc = chc__read_bytes(in, scratch, take, err);
2284
+ if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2285
+ left -= take;
2286
+ }
2287
+ *out = c;
2288
+ return CHC_OK;
2289
+ }
2290
+ if (t->n_children) {
2291
+ c->tuple.children = chc__calloc(al, t->n_children * sizeof *c->tuple.children, err);
2292
+ if (!c->tuple.children) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2293
+ c->tuple.arity = t->n_children;
2294
+ }
2295
+ for (size_t i = 0; i < t->n_children; i++) {
2296
+ chc_column **slot = &c->tuple.children[i];
2297
+ int rc = kinds ? chc__col_read_kinds(in, t->children[i], n_rows, kinds, slot, err)
2298
+ : chc__col_read(in, t->children[i], n_rows, slot, err);
2299
+ if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2300
+ }
2301
+ *out = c;
2302
+ return CHC_OK;
2303
+ }
2304
+
2305
+ /* Sparse body: varuint gap groups locating non-default rows, then those rows'
2306
+ * values densely. Gaps become zero bytes or empty strings, matching
2307
+ * ColumnSparse storage default even where type default differs (Enum) */
2308
+ static int
2309
+ chc__col_read_sparse(chc_in *in, const chc_type *t, size_t n_rows,
2310
+ chc_column **out, chc_err *err)
2311
+ {
2312
+ const chc_alloc *al = in->al;
2313
+ size_t es = chc_type_elem_size(t);
2314
+ /* Bounds positions, dense values & string offsets alike */
2315
+ size_t unit = es > sizeof(uint64_t) ? es : sizeof(uint64_t), bytes;
2316
+ if (chc__mul_size(n_rows, unit, &bytes))
2317
+ return chc__err_set(err, CHC_ERR_PROTOCOL, "column size overflow");
2318
+
2319
+ size_t *pos = NULL, n = 0, cap = 0, row = 0;
2320
+ chc_column *c = NULL;
2321
+ int rc;
2322
+ for (;;) {
2323
+ uint64_t g;
2324
+ if ((rc = chc__read_varuint(in, &g, err))) goto fail;
2325
+ bool end = g & CHC__SPARSE_END;
2326
+ g &= ~CHC__SPARSE_END;
2327
+ if (g >= CHC__SPARSE_END || g > n_rows - row) {
2328
+ rc = chc__err_set(err, CHC_ERR_PROTOCOL,
2329
+ "sparse offsets overrun %zu rows", n_rows);
2330
+ goto fail;
2331
+ }
2332
+ row += (size_t) g;
2333
+ if (end) break;
2334
+ if (row == n_rows) {
2335
+ rc = chc__err_set(err, CHC_ERR_PROTOCOL,
2336
+ "sparse offsets overrun %zu rows", n_rows);
2337
+ goto fail;
2338
+ }
2339
+ if (n == cap) {
2340
+ size_t ncap = cap ? cap * 2 : 64;
2341
+ if (ncap > n_rows) ncap = n_rows;
2342
+ size_t *np = chc__realloc(al, pos, cap * sizeof *pos, ncap * sizeof *pos, err);
2343
+ if (!np) { rc = CHC_ERR_OOM; goto fail; }
2344
+ pos = np;
2345
+ cap = ncap;
2346
+ }
2347
+ pos[n++] = row++;
2348
+ }
2349
+ if (row != n_rows) {
2350
+ rc = chc__err_set(err, CHC_ERR_PROTOCOL,
2351
+ "sparse offsets end at row %zu of %zu", row, n_rows);
2352
+ goto fail;
2353
+ }
2354
+
2355
+ rc = chc__col_read(in, t, n, &c, err);
2356
+ if (rc != CHC_OK) goto fail;
2357
+
2358
+ if (n < n_rows && c->layout == CHC_COL_FIXED) {
2359
+ uint8_t *dense = chc__calloc(al, n_rows * es, err);
2360
+ if (!dense) { rc = CHC_ERR_OOM; goto fail; }
2361
+ const uint8_t *src = c->fixed.data;
2362
+ for (size_t k = 0; k < n; k++)
2363
+ memcpy(dense + pos[k] * es, src + k * es, es);
2364
+ al->free(al->ud, c->fixed.data, n * es);
2365
+ c->fixed.data = dense;
2366
+ } else if (n < n_rows && c->layout == CHC_COL_STRING) {
2367
+ uint64_t *offs = chc__alloc(al, n_rows * sizeof *offs, err);
2368
+ if (!offs) { rc = CHC_ERR_OOM; goto fail; }
2369
+ uint64_t end = 0;
2370
+ for (size_t r = 0, k = 0; r < n_rows; r++) {
2371
+ if (k < n && pos[k] == r) end = c->str.offsets[k++];
2372
+ offs[r] = end;
2373
+ }
2374
+ al->free(al->ud, c->str.offsets, n * sizeof *c->str.offsets);
2375
+ c->str.offsets = offs;
2376
+ }
2377
+ c->n_rows = n_rows;
2378
+ al->free(al->ud, pos, cap * sizeof *pos);
2379
+ *out = c;
2380
+ return CHC_OK;
2381
+
2382
+ fail:
2383
+ chc__column_destroy(c, al);
2384
+ al->free(al->ud, pos, cap * sizeof *pos);
2385
+ return rc;
2386
+ }
2387
+
2388
+ /* Dispatch on serialization kinds read by chc__read_kinds, advancing cursor
2389
+ * in same order: Tuple own kind, ignored as ClickHouse ignores it, then each
2390
+ * element's */
2391
+ static int
2392
+ chc__col_read_kinds(chc_in *in, const chc_type *t, size_t n_rows,
2393
+ const uint8_t **kinds, chc_column **out, chc_err *err)
2394
+ {
2395
+ t = chc__serial_type(t);
2396
+ uint8_t kind = *(*kinds)++;
2397
+ if (t->kind == CHC_TUPLE) return chc__col_read_tuple(in, t, n_rows, kinds, out, err);
2398
+ if (kind == CHC__KIND_SPARSE) return chc__col_read_sparse(in, t, n_rows, out, err);
2399
+ return chc__col_read(in, t, n_rows, out, err);
2400
+ }
2401
+
2077
2402
  static int
2078
2403
  chc__col_read(chc_in *in, const chc_type *t,
2079
2404
  size_t n_rows, chc_column **out, chc_err *err)
@@ -2085,27 +2410,11 @@ chc__col_read(chc_in *in, const chc_type *t,
2085
2410
 
2086
2411
  switch (t->kind) {
2087
2412
  case CHC_STRING:
2088
- return chc__col_read_string(in, n_rows, out, err);
2089
-
2090
2413
  case CHC_JSON:
2091
- case CHC_OBJECT: {
2092
- /* JSON / Object('json') stream prefix: 8-byte LE serialization
2093
- * version (SerializationObject.cpp:275). Only STRING (=1) is in
2094
- * scope; other versions need the consumer to set
2095
- * output_format_native_write_json_as_string=1 on the SELECT.
2096
- * Body bytes per row are writeStringBinary, identical to a String
2097
- * column — reuse chc__col_read_string and keep CHC_COL_STRING
2098
- * layout so callers reuse string accessors. */
2099
- uint64_t version;
2100
- int rc = chc__read_u64_le(in, &version, err);
2101
- if (rc != CHC_OK) return rc;
2102
- if (version != 1)
2103
- return chc__err_set(err, CHC_ERR_TYPE,
2104
- "unsupported JSON serialization version %llu "
2105
- "(set output_format_native_write_json_as_string=1)",
2106
- (unsigned long long) version);
2414
+ case CHC_OBJECT:
2415
+ /* JSON/Object STRING-mode body rows use writeStringBinary, identical
2416
+ * to String. Prefix lives in chc__col_read_prefix */
2107
2417
  return chc__col_read_string(in, n_rows, out, err);
2108
- }
2109
2418
 
2110
2419
  case CHC_NULLABLE: {
2111
2420
  if (t->n_children != 1)
@@ -2127,11 +2436,14 @@ chc__col_read(chc_in *in, const chc_type *t,
2127
2436
  }
2128
2437
 
2129
2438
  case CHC_ARRAY:
2439
+ case CHC_NESTED:
2130
2440
  case CHC_MAP: {
2131
2441
  if (t->kind == CHC_ARRAY && t->n_children != 1)
2132
2442
  return chc__err_set(err, CHC_ERR_TYPE, "Array expects 1 child");
2133
2443
  if (t->kind == CHC_MAP && t->n_children != 2)
2134
2444
  return chc__err_set(err, CHC_ERR_TYPE, "Map expects 2 children");
2445
+ if (t->kind == CHC_NESTED && !t->n_children)
2446
+ return chc__err_set(err, CHC_ERR_TYPE, "Nested has no fields");
2135
2447
  chc_column *c = chc__calloc(al, sizeof *c, err);
2136
2448
  if (!c) return CHC_ERR_OOM;
2137
2449
  c->layout = CHC_COL_ARRAY;
@@ -2156,61 +2468,17 @@ chc__col_read(chc_in *in, const chc_type *t,
2156
2468
  (unsigned long long) total);
2157
2469
  }
2158
2470
  }
2159
- if (t->kind == CHC_ARRAY) {
2160
- int rc = chc__col_read(in, t->children[0], (size_t) total,
2161
- &c->array.values, err);
2162
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2163
- } else {
2164
- /* Map: synthesise an implicit Tuple(K, V) column. */
2165
- chc_column *tup = chc__calloc(al, sizeof *tup, err);
2166
- if (!tup) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2167
- tup->layout = CHC_COL_TUPLE;
2168
- tup->n_rows = (size_t) total;
2169
- tup->tuple.arity = 2;
2170
- tup->tuple.children = chc__calloc(al, 2 * sizeof *tup->tuple.children, err);
2171
- if (!tup->tuple.children) { chc__column_destroy(tup, al); chc__column_destroy(c, al); return CHC_ERR_OOM; }
2172
- int rc = chc__col_read(in, t->children[0], (size_t) total,
2173
- &tup->tuple.children[0], err);
2174
- if (rc != CHC_OK) { chc__column_destroy(tup, al); chc__column_destroy(c, al); return rc; }
2175
- rc = chc__col_read(in, t->children[1], (size_t) total,
2176
- &tup->tuple.children[1], err);
2177
- if (rc != CHC_OK) { chc__column_destroy(tup, al); chc__column_destroy(c, al); return rc; }
2178
- c->array.values = tup;
2179
- }
2471
+ /* ClickHouse sends Map and Nested values as arrays of tuples */
2472
+ int rc = t->kind == CHC_ARRAY
2473
+ ? chc__col_read(in, t->children[0], (size_t) total, &c->array.values, err)
2474
+ : chc__col_read_tuple(in, t, (size_t) total, NULL, &c->array.values, err);
2475
+ if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2180
2476
  *out = c;
2181
2477
  return CHC_OK;
2182
2478
  }
2183
2479
 
2184
- case CHC_TUPLE: {
2185
- chc_column *c = chc__calloc(al, sizeof *c, err);
2186
- if (!c) return CHC_ERR_OOM;
2187
- c->layout = CHC_COL_TUPLE;
2188
- c->n_rows = n_rows;
2189
- c->tuple.arity = t->n_children;
2190
- /* LOCAL PATCH (ch_connect): ClickHouse serializes the empty Tuple()
2191
- * as one UInt8 (zero) per row; skipping nothing desyncs the stream. */
2192
- if (t->n_children == 0) {
2193
- uint8_t scratch[256];
2194
- size_t left = n_rows;
2195
- while (left) {
2196
- size_t take = left > sizeof scratch ? sizeof scratch : left;
2197
- int rc = chc__read_bytes(in, scratch, take, err);
2198
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2199
- left -= take;
2200
- }
2201
- *out = c;
2202
- return CHC_OK;
2203
- }
2204
- c->tuple.children = chc__calloc(al, t->n_children * sizeof *c->tuple.children, err);
2205
- if (!c->tuple.children) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2206
- for (size_t i = 0; i < t->n_children; i++) {
2207
- int rc = chc__col_read(in, t->children[i], n_rows,
2208
- &c->tuple.children[i], err);
2209
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2210
- }
2211
- *out = c;
2212
- return CHC_OK;
2213
- }
2480
+ case CHC_TUPLE:
2481
+ return chc__col_read_tuple(in, t, n_rows, NULL, out, err);
2214
2482
 
2215
2483
  case CHC_QBIT: {
2216
2484
  /* Wire form is Tuple(FixedString(ceil(N/8)) x element_size): one
@@ -2223,9 +2491,9 @@ chc__col_read(chc_in *in, const chc_type *t,
2223
2491
  if (!c) return CHC_ERR_OOM;
2224
2492
  c->layout = CHC_COL_TUPLE;
2225
2493
  c->n_rows = n_rows;
2226
- c->tuple.arity = bits;
2227
2494
  c->tuple.children = chc__calloc(al, bits * sizeof *c->tuple.children, err);
2228
2495
  if (!c->tuple.children) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2496
+ c->tuple.arity = bits;
2229
2497
  for (size_t i = 0; i < bits; i++) {
2230
2498
  int rc = chc__col_read_fixed(in, bytes_per_plane, n_rows,
2231
2499
  &c->tuple.children[i], err);
@@ -2305,9 +2573,9 @@ chc__col_read(chc_in *in, const chc_type *t,
2305
2573
 
2306
2574
  if (nullable_wrap) {
2307
2575
  /* Wire convention: slot 0 of the inner-typed dict is the NULL
2308
- * sentinel (clickhouse-cpp/columns/lowcardinality.cpp 287-295).
2309
- * Wrap the dict in a Nullable column so the caller's standard
2310
- * null-map dispatch covers the LC(Nullable) case. */
2576
+ * sentinel (see the LowCardinality section of the Native Format
2577
+ * spec). Wrap the dict in a Nullable column so the caller's
2578
+ * standard null-map dispatch covers the LC(Nullable) case. */
2311
2579
  chc_column *wrapped = chc__calloc(al, sizeof *wrapped, err);
2312
2580
  if (!wrapped) { chc__column_destroy(inner_dict, al); chc__column_destroy(c, al); return CHC_ERR_OOM; }
2313
2581
  wrapped->layout = CHC_COL_NULLABLE;
@@ -2351,17 +2619,18 @@ chc__col_read(chc_in *in, const chc_type *t,
2351
2619
  }
2352
2620
 
2353
2621
  case CHC_SIMPLE_AGGREGATE_FUNCTION:
2354
- /* Wire form is the inner type's stream. Last child is the data type. */
2622
+ /* ClickHouse stores values using this function's first argument type */
2355
2623
  if (t->n_children < 1)
2356
2624
  return chc__err_set(err, CHC_ERR_TYPE, "SimpleAggregateFunction has no inner type");
2357
- return chc__col_read(in, t->children[t->n_children - 1], n_rows, out, err);
2625
+ return chc__col_read(in, t->children[0], n_rows, out, err);
2358
2626
 
2359
- /* Geo types: aliases for nested Array layers terminating in
2360
- * Tuple(Float64, Float64). Per clickhouse-cpp factory.cpp 120-130. */
2361
- case CHC_POINT: return chc__col_read_geo(in, 0, n_rows, out, err);
2362
- case CHC_RING: return chc__col_read_geo(in, 1, n_rows, out, err);
2363
- case CHC_POLYGON: return chc__col_read_geo(in, 2, n_rows, out, err);
2364
- case CHC_MULTI_POLYGON: return chc__col_read_geo(in, 3, n_rows, out, err);
2627
+ case CHC_POINT:
2628
+ case CHC_RING:
2629
+ case CHC_LINE_STRING:
2630
+ case CHC_POLYGON:
2631
+ case CHC_MULTI_LINE_STRING:
2632
+ case CHC_MULTI_POLYGON:
2633
+ return chc__col_read(in, chc__geo_alias(t), n_rows, out, err);
2365
2634
 
2366
2635
  case CHC_NOTHING:
2367
2636
  case CHC_VOID: {
@@ -2370,16 +2639,8 @@ chc__col_read(chc_in *in, const chc_type *t,
2370
2639
  c->layout = CHC_COL_NOTHING;
2371
2640
  c->n_rows = n_rows;
2372
2641
  /* Wire shape for Nothing is a sequence of UInt8 bytes per row. */
2373
- if (n_rows) {
2374
- uint8_t throwaway[256];
2375
- size_t left = n_rows;
2376
- while (left) {
2377
- size_t take = left < sizeof throwaway ? left : sizeof throwaway;
2378
- int rc = chc__read_bytes(in, throwaway, take, err);
2379
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2380
- left -= take;
2381
- }
2382
- }
2642
+ int rc = chc__skip_bytes(in, n_rows, err);
2643
+ if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2383
2644
  *out = c;
2384
2645
  return CHC_OK;
2385
2646
  }
@@ -2393,60 +2654,74 @@ chc__col_read(chc_in *in, const chc_type *t,
2393
2654
  }
2394
2655
  }
2395
2656
 
2396
- static int
2397
- chc__col_read_geo(chc_in *in, int depth, size_t n_rows,
2398
- chc_column **out, chc_err *err)
2657
+ /* -------- block reader ---------- */
2658
+
2659
+ /* Serialization kind count, Tuple emits own kind then each element's */
2660
+ static size_t
2661
+ chc__kinds_len(const chc_type *t)
2399
2662
  {
2400
- const chc_alloc *al = in->al;
2401
- if (depth == 0) {
2402
- /* Point = Tuple(Float64, Float64). */
2403
- chc_column *c = chc__calloc(al, sizeof *c, err);
2404
- if (!c) return CHC_ERR_OOM;
2405
- c->layout = CHC_COL_TUPLE;
2406
- c->n_rows = n_rows;
2407
- c->tuple.arity = 2;
2408
- c->tuple.children = chc__calloc(al, 2 * sizeof *c->tuple.children, err);
2409
- if (!c->tuple.children) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2410
- for (int i = 0; i < 2; i++) {
2411
- int rc = chc__col_read_fixed(in, 8, n_rows, &c->tuple.children[i], err);
2412
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2663
+ t = chc__serial_type(t);
2664
+ size_t n = 1;
2665
+ if (t->kind == CHC_TUPLE)
2666
+ for (size_t i = 0; i < t->n_children; i++)
2667
+ n += chc__kinds_len(t->children[i]);
2668
+ return n;
2669
+ }
2670
+
2671
+ /* Validate kinds in chc__kinds_len order. Sparse applies to leaves stored
2672
+ * as fixed width values, String or Nothing. Other kinds carry payloads not
2673
+ * handled here */
2674
+ static int
2675
+ chc__kinds_check(const chc_type *t, const uint8_t **kinds, chc_err *err)
2676
+ {
2677
+ t = chc__serial_type(t);
2678
+ uint8_t kind = *(*kinds)++;
2679
+ if (kind > CHC__KIND_SPARSE)
2680
+ return chc__err_set(err, CHC_ERR_PROTOCOL,
2681
+ "unsupported serialization kind %u", (unsigned) kind);
2682
+ if (t->kind == CHC_TUPLE) {
2683
+ for (size_t i = 0; i < t->n_children; i++) {
2684
+ int rc = chc__kinds_check(t->children[i], kinds, err);
2685
+ if (rc != CHC_OK) return rc;
2413
2686
  }
2414
- *out = c;
2415
2687
  return CHC_OK;
2416
2688
  }
2417
- /* Array(geo(depth-1)). */
2418
- chc_column *c = chc__calloc(al, sizeof *c, err);
2419
- if (!c) return CHC_ERR_OOM;
2420
- c->layout = CHC_COL_ARRAY;
2421
- c->n_rows = n_rows;
2422
- uint64_t total = 0;
2423
- if (n_rows) {
2424
- size_t offs_bytes;
2425
- if (chc__mul_size(n_rows, sizeof(uint64_t), &offs_bytes)) {
2426
- chc__column_destroy(c, al);
2427
- return chc__err_set(err, CHC_ERR_PROTOCOL, "array offsets size overflow");
2428
- }
2429
- c->array.offsets = chc__alloc(al, offs_bytes, err);
2430
- if (!c->array.offsets) { chc__column_destroy(c, al); return CHC_ERR_OOM; }
2431
- int rc = chc__read_bytes(in, c->array.offsets, offs_bytes, err);
2432
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2433
- chc__swap_offsets(c->array.offsets, n_rows);
2434
- total = c->array.offsets[n_rows - 1];
2435
- if (total > CHC_MAX_NUM_ROWS) {
2436
- chc__column_destroy(c, al);
2437
- return chc__err_set(err, CHC_ERR_PROTOCOL,
2438
- "array nested length too large: %llu",
2439
- (unsigned long long) total);
2440
- }
2689
+ if (kind == CHC__KIND_SPARSE && !chc_type_elem_size(t) && t->kind != CHC_STRING
2690
+ && t->kind != CHC_NOTHING && t->kind != CHC_VOID) {
2691
+ size_t nl;
2692
+ const char *nm = chc_type_name(t, &nl);
2693
+ return chc__err_set(err, CHC_ERR_PROTOCOL,
2694
+ "sparse serialization unsupported for %.*s", (int) nl, nm ? nm : "");
2441
2695
  }
2442
- int rc = chc__col_read_geo(in, depth - 1, (size_t) total,
2443
- &c->array.values, err);
2444
- if (rc != CHC_OK) { chc__column_destroy(c, al); return rc; }
2445
- *out = c;
2446
2696
  return CHC_OK;
2447
2697
  }
2448
2698
 
2449
- /* -------- block reader ---------- */
2699
+ /* Read has_custom flag & serialization kinds following column type. *out
2700
+ * stays NULL when every kind is DEFAULT */
2701
+ static int
2702
+ chc__read_kinds(chc_in *in, const chc_type *t, uint8_t **out, size_t *out_len,
2703
+ chc_err *err)
2704
+ {
2705
+ uint8_t has_custom;
2706
+ int rc = chc__read_byte(in, &has_custom, err);
2707
+ if (rc != CHC_OK || !has_custom) return rc;
2708
+ if (has_custom != 1)
2709
+ return chc__err_set(err, CHC_ERR_PROTOCOL,
2710
+ "invalid custom serialization flag %u", (unsigned) has_custom);
2711
+ size_t n = chc__kinds_len(t);
2712
+ uint8_t *k = chc__alloc(in->al, n, err);
2713
+ if (!k) return CHC_ERR_OOM;
2714
+ const uint8_t *cursor = k;
2715
+ rc = chc__read_bytes(in, k, n, err);
2716
+ if (rc == CHC_OK) rc = chc__kinds_check(t, &cursor, err);
2717
+ if (rc != CHC_OK || !memchr(k, CHC__KIND_SPARSE, n)) {
2718
+ in->al->free(in->al->ud, k, n);
2719
+ return rc;
2720
+ }
2721
+ *out = k;
2722
+ *out_len = n;
2723
+ return CHC_OK;
2724
+ }
2450
2725
 
2451
2726
  struct chc_block {
2452
2727
  size_t n_columns;
@@ -2600,7 +2875,8 @@ chc__block_resume_in(chc_in *in, const chc_alloc *al,
2600
2875
 
2601
2876
  size_t nrows = b->n_rows;
2602
2877
  for (size_t i = *next_col; i < b->n_columns; i++) {
2603
- char *type_name = NULL; size_t type_len = 0;
2878
+ char *type_name; size_t type_len;
2879
+ uint8_t *kinds = NULL; size_t kinds_len = 0;
2604
2880
 
2605
2881
  /* Checkpoint column start so a mid-column would-block rewinds here,
2606
2882
  * dropping completed columns' wire bytes on the caller's next submit. */
@@ -2611,37 +2887,33 @@ chc__block_resume_in(chc_in *in, const chc_alloc *al,
2611
2887
 
2612
2888
  rc = chc__read_string(in, &type_name, &type_len, err);
2613
2889
  if (rc != CHC_OK) goto col_fail;
2890
+ rc = chc_type_parse(type_name, type_len, al, &b->types[i], err);
2891
+ al->free(al->ud, type_name, type_len + 1);
2892
+ if (rc != CHC_OK) goto col_fail;
2614
2893
 
2615
2894
  if (opts->has_custom_serialization) {
2616
- uint8_t hcs;
2617
- rc = chc__read_byte(in, &hcs, err);
2895
+ rc = chc__read_kinds(in, b->types[i], &kinds, &kinds_len, err);
2618
2896
  if (rc != CHC_OK) goto col_fail;
2619
- if (hcs) {
2620
- rc = chc__err_set(err, CHC_ERR_PROTOCOL,
2621
- "custom serialization not supported on column '%s'", b->names[i]);
2622
- goto col_fail;
2623
- }
2624
2897
  }
2625
2898
 
2626
- rc = chc_type_parse(type_name, type_len, al, &b->types[i], err);
2627
- al->free(al->ud, type_name, type_len + 1);
2628
- type_name = NULL;
2629
- if (rc != CHC_OK) goto col_fail;
2630
-
2899
+ /* Zero-row column carries kinds but no body, not even sparse offsets */
2631
2900
  if (nrows) {
2632
2901
  rc = chc__col_read_prefix(in, b->types[i], err);
2633
2902
  if (rc != CHC_OK) goto col_fail;
2634
2903
 
2635
- rc = chc__col_read(in, b->types[i], nrows, &b->columns[i], err);
2904
+ const uint8_t *cursor = kinds;
2905
+ rc = kinds ? chc__col_read_kinds(in, b->types[i], nrows, &cursor, &b->columns[i], err)
2906
+ : chc__col_read(in, b->types[i], nrows, &b->columns[i], err);
2636
2907
  if (rc != CHC_OK) goto col_fail;
2637
2908
  }
2909
+ al->free(al->ud, kinds, kinds_len);
2638
2910
  continue;
2639
2911
 
2640
2912
  col_fail:
2913
+ al->free(al->ud, kinds, kinds_len);
2641
2914
  if (ioless && rc == CHC_WOULD_BLOCK) {
2642
2915
  /* Retain columns [0,i); reset slot i to a destroy-safe NULL state
2643
2916
  * and rewind so column i re-parses from its checkpoint. */
2644
- if (type_name) al->free(al->ud, type_name, type_len + 1);
2645
2917
  if (b->names[i]) {
2646
2918
  al->free(al->ud, b->names[i], b->name_lens[i] + 1);
2647
2919
  b->names[i] = NULL;
@@ -2655,7 +2927,6 @@ chc__block_resume_in(chc_in *in, const chc_alloc *al,
2655
2927
  *next_col = i;
2656
2928
  return CHC_WOULD_BLOCK;
2657
2929
  }
2658
- if (type_name) al->free(al->ud, type_name, type_len + 1);
2659
2930
  chc_block_destroy(b, al);
2660
2931
  *blk = NULL;
2661
2932
  return rc;
@@ -2671,7 +2942,7 @@ header_fail:
2671
2942
  return rc;
2672
2943
  }
2673
2944
 
2674
- /* Block read from an already-initialised chc_in. Also used by
2945
+ /* Block read from an already-initialized chc_in. Also used by
2675
2946
  * clickhouse-client.h's recv_packet (persistent buffer). Thin non-looping
2676
2947
  * wrapper over chc__block_resume_in. Returns 0 with *out == NULL on clean EOF
2677
2948
  * at block boundary (only when opts->has_block_info is false; TCP path has no
@@ -2711,410 +2982,93 @@ chc_block_read(chc_in *in, const chc_alloc *al,
2711
2982
 
2712
2983
  /* -------- block writer ---------- */
2713
2984
 
2714
- typedef enum {
2715
- CHC__BLD_FIXED = 1,
2716
- CHC__BLD_STRING = 2,
2717
- CHC__BLD_NULL_FIXED = 3,
2718
- CHC__BLD_NULL_STRING = 4,
2719
- CHC__BLD_ARRAY_FIXED = 5,
2720
- CHC__BLD_ARRAY_STRING = 6,
2721
- CHC__BLD_LC_STRING = 7,
2722
- CHC__BLD_JSON_STRING = 8,
2723
- CHC__BLD_ARRAY_NESTED_FIXED = 9,
2724
- CHC__BLD_ARRAY_NESTED_STRING = 10,
2725
- } chc__bld_kind;
2726
-
2727
- typedef struct {
2728
- const char *name;
2729
- size_t name_len;
2730
- const chc_type *type; /* NULL only for legacy STRING entries */
2731
- chc__bld_kind kind;
2732
- size_t n_rows;
2733
- size_t inner_n; /* element count of the inner array/string/dict body */
2734
- /* Pointers into caller-owned memory; library never copies. */
2735
- /* Base representation: fixed-width xor variable-length. */
2736
- union {
2737
- struct { const void *data; size_t elem_size; } fixed; /* *_FIXED */
2738
- struct { const uint64_t *offsets; const uint8_t *data; } str; /* *_STRING / LC dict */
2739
- };
2740
- /* Structural modifier over base; absent for plain FIXED / STRING / JSON. */
2741
- union {
2742
- struct { const uint8_t *null_map; } nullable; /* NULL_* */
2743
- struct { const uint64_t *offsets; } array; /* ARRAY_FIXED / ARRAY_STRING (cumulative ends) */
2744
- struct { /* ARRAY_NESTED_*, ndim >= 2 */
2745
- int ndim;
2746
- const uint64_t * const *level_offsets; /* ndim cumulative-end arrays */
2747
- const size_t *level_offsets_len; /* count per level */
2748
- } nested;
2749
- struct { int key_size; const void *keys; } lc; /* LC_STRING */
2750
- };
2751
- } chc__col_entry;
2752
-
2753
- struct chc_block_builder {
2754
- const chc_alloc *al; /* captured at init */
2755
- chc__col_entry *cols;
2756
- size_t n_cols;
2757
- size_t cap;
2758
- size_t n_rows; /* common across all columns */
2759
- bool n_rows_set;
2760
- };
2761
-
2762
- int
2763
- chc_block_builder_init(chc_block_builder **out, const chc_alloc *al,
2764
- chc_err *err)
2765
- {
2766
- chc_block_builder *bb = chc__calloc(al, sizeof *bb, err);
2767
- if (!bb) return CHC_ERR_OOM;
2768
- bb->al = al;
2769
- *out = bb;
2770
- return CHC_OK;
2771
- }
2772
-
2773
2985
  void
2774
- chc_block_builder_destroy(chc_block_builder *bb)
2986
+ chc_block_builder_init(chc_block_builder *bb, chc_block_col *cols)
2775
2987
  {
2776
- if (!bb) return;
2777
- const chc_alloc *al = bb->al;
2778
- al->free(al->ud, bb->cols, bb->cap * sizeof *bb->cols);
2779
- al->free(al->ud, bb, sizeof *bb);
2988
+ *bb = (chc_block_builder) { .cols = cols };
2780
2989
  }
2781
2990
 
2782
- static int
2783
- chc__bld_grow(chc_block_builder *bb, chc_err *err)
2784
- {
2785
- if (bb->n_cols < bb->cap) return CHC_OK;
2786
- size_t new_cap = bb->cap ? bb->cap * 2 : 4;
2787
- chc__col_entry *p = chc__realloc(bb->al, bb->cols,
2788
- bb->cap * sizeof *bb->cols,
2789
- new_cap * sizeof *bb->cols, err);
2790
- if (!p) return CHC_ERR_OOM;
2791
- bb->cols = p;
2792
- bb->cap = new_cap;
2793
- return CHC_OK;
2794
- }
2991
+ /* -------- compositional column builders ---------- */
2992
+ /* Cast away const for reader-compatible tree shape; writer never mutates
2993
+ * caller trees */
2795
2994
 
2796
- static int
2797
- chc__bld_check_rows(chc_block_builder *bb, size_t n_rows, chc_err *err)
2995
+ chc_column
2996
+ chc_build_fixed(const void *data, size_t elem_size, size_t n_rows)
2798
2997
  {
2799
- if (!bb->n_rows_set) { bb->n_rows = n_rows; bb->n_rows_set = true; return CHC_OK; }
2800
- if (bb->n_rows != n_rows)
2801
- return chc__err_set(err, CHC_ERR_USAGE,
2802
- "block_builder: row count mismatch (%zu vs %zu)", bb->n_rows, n_rows);
2803
- return CHC_OK;
2998
+ return (chc_column) {
2999
+ .layout = CHC_COL_FIXED,
3000
+ .n_rows = n_rows,
3001
+ .fixed.data = (void *) data,
3002
+ .fixed.elem_size = elem_size,
3003
+ };
2804
3004
  }
2805
3005
 
2806
- static int
2807
- chc__bld_add(chc_block_builder *bb, const char *name, size_t name_len,
2808
- const chc_type *type, chc__bld_kind kind, size_t n_rows,
2809
- chc__col_entry **out, chc_err *err)
3006
+ chc_column
3007
+ chc_build_string(const uint64_t *offsets, const uint8_t *data, size_t n_rows)
2810
3008
  {
2811
- int rc = chc__bld_check_rows(bb, n_rows, err);
2812
- if (rc != CHC_OK) return rc;
2813
- rc = chc__bld_grow(bb, err);
2814
- if (rc != CHC_OK) return rc;
2815
- chc__col_entry *e = &bb->cols[bb->n_cols++];
2816
- *e = (chc__col_entry) {
2817
- .name = name, .name_len = name_len, .type = type,
2818
- .kind = kind, .n_rows = n_rows,
3009
+ return (chc_column) {
3010
+ .layout = CHC_COL_STRING,
3011
+ .n_rows = n_rows,
3012
+ .str.offsets = (uint64_t *) offsets,
3013
+ .str.data = (uint8_t *) data,
2819
3014
  };
2820
- *out = e;
2821
- return CHC_OK;
2822
3015
  }
2823
3016
 
2824
- int
2825
- chc_block_builder_append_fixed(chc_block_builder *bb,
2826
- const char *name, size_t name_len,
2827
- const chc_type *t,
2828
- const void *data, size_t n_rows,
2829
- chc_err *err)
3017
+ chc_column
3018
+ chc_build_nullable(const uint8_t *null_map, chc_column *inner)
2830
3019
  {
2831
- size_t es = chc_type_elem_size(t);
2832
- if (!es) return chc__err_set(err, CHC_ERR_TYPE,
2833
- "append_fixed: type is not fixed-size");
2834
- chc__col_entry *e;
2835
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_FIXED,
2836
- n_rows, &e, err);
2837
- if (rc != CHC_OK) return rc;
2838
- e->fixed.data = data;
2839
- e->fixed.elem_size = es;
2840
- return CHC_OK;
2841
- }
2842
-
2843
- int
2844
- chc_block_builder_append_string(chc_block_builder *bb,
2845
- const char *name, size_t name_len,
2846
- const uint64_t *offsets,
2847
- const uint8_t *data, size_t n_rows,
2848
- chc_err *err)
2849
- {
2850
- chc__col_entry *e;
2851
- int rc = chc__bld_add(bb, name, name_len, NULL, CHC__BLD_STRING,
2852
- n_rows, &e, err);
2853
- if (rc != CHC_OK) return rc;
2854
- e->str.offsets = offsets;
2855
- e->str.data = data;
2856
- e->inner_n = n_rows;
2857
- return CHC_OK;
3020
+ return (chc_column) {
3021
+ .layout = CHC_COL_NULLABLE,
3022
+ .n_rows = inner ? inner->n_rows : 0,
3023
+ .nullable.null_map = (uint8_t *) null_map,
3024
+ .nullable.inner = inner,
3025
+ };
2858
3026
  }
2859
3027
 
2860
- /* Extract the inner fixed-elem size from a Nullable(<fixed>) /
2861
- * Array(<fixed>) type. 0 if `t` is not the expected shape. */
2862
- static size_t
2863
- chc__bld_inner_fixed_size(const chc_type *t, chc_kind outer)
3028
+ chc_column
3029
+ chc_build_array(const uint64_t *offsets, size_t n_rows, chc_column *values)
2864
3030
  {
2865
- if (!t || t->kind != outer || t->n_children != 1) return 0;
2866
- return chc_type_elem_size(t->children[0]);
3031
+ return (chc_column) {
3032
+ .layout = CHC_COL_ARRAY,
3033
+ .n_rows = n_rows,
3034
+ .array.offsets = (uint64_t *) offsets,
3035
+ .array.values = values,
3036
+ };
2867
3037
  }
2868
3038
 
2869
- /* True iff `t` is Array(String) / Nullable(String). */
2870
- static bool
2871
- chc__bld_inner_is_string(const chc_type *t, chc_kind outer)
3039
+ chc_column
3040
+ chc_build_tuple(chc_column **children, size_t arity)
2872
3041
  {
2873
- return t && t->kind == outer && t->n_children == 1
2874
- && t->children[0]->kind == CHC_STRING;
2875
- }
2876
-
2877
- /* True iff `t` is LowCardinality(String) or LowCardinality(Nullable(String)). */
2878
- static bool
2879
- chc__bld_lc_inner_is_string(const chc_type *t)
2880
- {
2881
- if (!t || t->kind != CHC_LOW_CARDINALITY || t->n_children != 1) return false;
2882
- const chc_type *inner = t->children[0];
2883
- if (inner->kind == CHC_STRING) return true;
2884
- if (inner->kind == CHC_NULLABLE && inner->n_children == 1
2885
- && inner->children[0]->kind == CHC_STRING)
2886
- return true;
2887
- return false;
2888
- }
2889
-
2890
- int
2891
- chc_block_builder_append_nullable_fixed(chc_block_builder *bb,
2892
- const char *name, size_t name_len,
2893
- const chc_type *t,
2894
- const uint8_t *null_map,
2895
- const void *inner_data,
2896
- size_t n_rows, chc_err *err)
2897
- {
2898
- size_t es = chc__bld_inner_fixed_size(t, CHC_NULLABLE);
2899
- if (!es) return chc__err_set(err, CHC_ERR_TYPE,
2900
- "append_nullable_fixed: type is not Nullable(<fixed>)");
2901
- chc__col_entry *e;
2902
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_NULL_FIXED,
2903
- n_rows, &e, err);
2904
- if (rc != CHC_OK) return rc;
2905
- e->nullable.null_map = null_map;
2906
- e->fixed.data = inner_data;
2907
- e->fixed.elem_size = es;
2908
- return CHC_OK;
2909
- }
2910
-
2911
- int
2912
- chc_block_builder_append_nullable_string(chc_block_builder *bb,
2913
- const char *name, size_t name_len,
2914
- const chc_type *t,
2915
- const uint8_t *null_map,
2916
- const uint64_t *inner_offsets,
2917
- const uint8_t *inner_data,
2918
- size_t n_rows, chc_err *err)
2919
- {
2920
- if (!chc__bld_inner_is_string(t, CHC_NULLABLE))
2921
- return chc__err_set(err, CHC_ERR_TYPE,
2922
- "append_nullable_string: type is not Nullable(String)");
2923
- chc__col_entry *e;
2924
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_NULL_STRING,
2925
- n_rows, &e, err);
2926
- if (rc != CHC_OK) return rc;
2927
- e->nullable.null_map = null_map;
2928
- e->str.offsets = inner_offsets;
2929
- e->str.data = inner_data;
2930
- e->inner_n = n_rows;
2931
- return CHC_OK;
2932
- }
2933
-
2934
- int
2935
- chc_block_builder_append_array_fixed(chc_block_builder *bb,
2936
- const char *name, size_t name_len,
2937
- const chc_type *t,
2938
- const uint64_t *offsets,
2939
- const void *values,
2940
- size_t n_rows, chc_err *err)
2941
- {
2942
- size_t es = chc__bld_inner_fixed_size(t, CHC_ARRAY);
2943
- if (!es) return chc__err_set(err, CHC_ERR_TYPE,
2944
- "append_array_fixed: type is not Array(<fixed>)");
2945
- chc__col_entry *e;
2946
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_ARRAY_FIXED,
2947
- n_rows, &e, err);
2948
- if (rc != CHC_OK) return rc;
2949
- e->array.offsets = offsets;
2950
- e->fixed.data = values;
2951
- e->fixed.elem_size = es;
2952
- e->inner_n = n_rows ? (size_t) offsets[n_rows - 1] : 0;
2953
- return CHC_OK;
2954
- }
2955
-
2956
- int
2957
- chc_block_builder_append_array_string(chc_block_builder *bb,
2958
- const char *name, size_t name_len,
2959
- const chc_type *t,
2960
- const uint64_t *offsets,
2961
- const uint64_t *values_offsets,
2962
- const uint8_t *values_data,
2963
- size_t n_rows, chc_err *err)
2964
- {
2965
- if (!chc__bld_inner_is_string(t, CHC_ARRAY))
2966
- return chc__err_set(err, CHC_ERR_TYPE,
2967
- "append_array_string: type is not Array(String)");
2968
- chc__col_entry *e;
2969
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_ARRAY_STRING,
2970
- n_rows, &e, err);
2971
- if (rc != CHC_OK) return rc;
2972
- e->array.offsets = offsets;
2973
- e->str.offsets = values_offsets;
2974
- e->str.data = values_data;
2975
- e->inner_n = n_rows ? (size_t) offsets[n_rows - 1] : 0;
2976
- return CHC_OK;
3042
+ return (chc_column) {
3043
+ .layout = CHC_COL_TUPLE,
3044
+ .n_rows = arity ? children[0]->n_rows : 0,
3045
+ .tuple.children = children,
3046
+ .tuple.arity = arity,
3047
+ };
2977
3048
  }
2978
3049
 
2979
- /* Walk past ndim Array(...) layers, return leaf type or NULL on
2980
- * shape mismatch */
2981
- static const chc_type *
2982
- chc__bld_array_leaf(const chc_type *t, int ndim)
3050
+ chc_column
3051
+ chc_build_lc(int key_size, const void *keys, size_t n_rows, chc_column *dict)
2983
3052
  {
2984
- while (ndim-- > 0) {
2985
- if (!t || t->kind != CHC_ARRAY || t->n_children != 1) return NULL;
2986
- t = t->children[0];
2987
- }
2988
- return t;
2989
- }
2990
-
2991
- int
2992
- chc_block_builder_append_array_nested_fixed(chc_block_builder *bb,
2993
- const char *name, size_t name_len,
2994
- const chc_type *t,
2995
- int ndim,
2996
- const uint64_t * const *level_offsets,
2997
- const size_t *level_offsets_len,
2998
- const void *values,
2999
- size_t n_rows, chc_err *err)
3000
- {
3001
- if (ndim < 2)
3002
- return chc__err_set(err, CHC_ERR_USAGE,
3003
- "append_array_nested_fixed: ndim must be >= 2");
3004
- const chc_type *leaf = chc__bld_array_leaf(t, ndim);
3005
- if (!leaf)
3006
- return chc__err_set(err, CHC_ERR_TYPE,
3007
- "append_array_nested_fixed: type does not match ndim");
3008
- size_t es = chc_type_elem_size(leaf);
3009
- if (!es)
3010
- return chc__err_set(err, CHC_ERR_TYPE,
3011
- "append_array_nested_fixed: leaf is not fixed-size");
3012
- if (n_rows != level_offsets_len[0])
3013
- return chc__err_set(err, CHC_ERR_USAGE,
3014
- "append_array_nested_fixed: n_rows != level_offsets_len[0]");
3015
- chc__col_entry *e;
3016
- int rc = chc__bld_add(bb, name, name_len, t,
3017
- CHC__BLD_ARRAY_NESTED_FIXED, n_rows, &e, err);
3018
- if (rc != CHC_OK) return rc;
3019
- e->nested.ndim = ndim;
3020
- e->nested.level_offsets = level_offsets;
3021
- e->nested.level_offsets_len = level_offsets_len;
3022
- e->fixed.data = values;
3023
- e->fixed.elem_size = es;
3024
- /* inner_n holds leaf element count: last cumulative end of innermost level */
3025
- {
3026
- size_t ilen = level_offsets_len[ndim - 1];
3027
- e->inner_n = ilen ? (size_t) level_offsets[ndim - 1][ilen - 1] : 0;
3028
- }
3029
- return CHC_OK;
3030
- }
3031
-
3032
- int
3033
- chc_block_builder_append_array_nested_string(chc_block_builder *bb,
3034
- const char *name, size_t name_len,
3035
- const chc_type *t,
3036
- int ndim,
3037
- const uint64_t * const *level_offsets,
3038
- const size_t *level_offsets_len,
3039
- const uint64_t *values_offsets,
3040
- const uint8_t *values_data,
3041
- size_t n_rows, chc_err *err)
3042
- {
3043
- if (ndim < 2)
3044
- return chc__err_set(err, CHC_ERR_USAGE,
3045
- "append_array_nested_string: ndim must be >= 2");
3046
- const chc_type *leaf = chc__bld_array_leaf(t, ndim);
3047
- if (!leaf || leaf->kind != CHC_STRING)
3048
- return chc__err_set(err, CHC_ERR_TYPE,
3049
- "append_array_nested_string: leaf is not String");
3050
- if (n_rows != level_offsets_len[0])
3051
- return chc__err_set(err, CHC_ERR_USAGE,
3052
- "append_array_nested_string: n_rows != level_offsets_len[0]");
3053
- chc__col_entry *e;
3054
- int rc = chc__bld_add(bb, name, name_len, t,
3055
- CHC__BLD_ARRAY_NESTED_STRING, n_rows, &e, err);
3056
- if (rc != CHC_OK) return rc;
3057
- e->nested.ndim = ndim;
3058
- e->nested.level_offsets = level_offsets;
3059
- e->nested.level_offsets_len = level_offsets_len;
3060
- e->str.offsets = values_offsets;
3061
- e->str.data = values_data;
3062
- {
3063
- size_t ilen = level_offsets_len[ndim - 1];
3064
- e->inner_n = ilen ? (size_t) level_offsets[ndim - 1][ilen - 1] : 0;
3065
- }
3066
- return CHC_OK;
3067
- }
3068
-
3069
- int
3070
- chc_block_builder_append_json_string(chc_block_builder *bb,
3071
- const char *name, size_t name_len,
3072
- const chc_type *t,
3073
- const uint64_t *offsets,
3074
- const uint8_t *data,
3075
- size_t n_rows, chc_err *err)
3076
- {
3077
- if (!t || t->kind != CHC_JSON)
3078
- return chc__err_set(err, CHC_ERR_TYPE,
3079
- "append_json_string requires CHC_JSON type, got %d",
3080
- (int) (t ? t->kind : 0));
3081
- chc__col_entry *e;
3082
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_JSON_STRING,
3083
- n_rows, &e, err);
3084
- if (rc != CHC_OK) return rc;
3085
- e->str.offsets = offsets;
3086
- e->str.data = data;
3087
- e->inner_n = n_rows;
3088
- return CHC_OK;
3053
+ return (chc_column) {
3054
+ .layout = CHC_COL_LOW_CARDINALITY,
3055
+ .n_rows = n_rows,
3056
+ .lc.key_size = key_size,
3057
+ .lc.keys = (void *) keys,
3058
+ .lc.dict = dict,
3059
+ .lc.dict_n = dict ? dict->n_rows : 0,
3060
+ };
3089
3061
  }
3090
3062
 
3091
- int
3092
- chc_block_builder_append_low_cardinality_string(chc_block_builder *bb,
3093
- const char *name, size_t name_len,
3094
- const chc_type *t,
3095
- int key_size,
3096
- const void *keys,
3097
- const uint64_t *dict_offsets,
3098
- const uint8_t *dict_data,
3099
- size_t dict_n,
3100
- size_t n_rows, chc_err *err)
3101
- {
3102
- if (!chc__bld_lc_inner_is_string(t))
3103
- return chc__err_set(err, CHC_ERR_TYPE,
3104
- "append_low_cardinality_string: type is not LowCardinality(String) or LowCardinality(Nullable(String))");
3105
- if (key_size != 1 && key_size != 2 && key_size != 4 && key_size != 8)
3106
- return chc__err_set(err, CHC_ERR_USAGE,
3107
- "append_low_cardinality_string: key_size must be 1/2/4/8 (got %d)", key_size);
3108
- chc__col_entry *e;
3109
- int rc = chc__bld_add(bb, name, name_len, t, CHC__BLD_LC_STRING,
3110
- n_rows, &e, err);
3111
- if (rc != CHC_OK) return rc;
3112
- e->lc.key_size = key_size;
3113
- e->lc.keys = keys;
3114
- e->str.offsets = dict_offsets;
3115
- e->str.data = dict_data;
3116
- e->inner_n = dict_n;
3117
- return CHC_OK;
3063
+ void
3064
+ chc_block_builder_append(chc_block_builder *bb,
3065
+ const char *name, size_t name_len,
3066
+ const chc_type *t, const chc_column *col)
3067
+ {
3068
+ bb->n_rows = col->n_rows;
3069
+ bb->cols[bb->n_cols++] = (chc_block_col) {
3070
+ .name = name, .name_len = name_len, .type = t, .col = col,
3071
+ };
3118
3072
  }
3119
3073
 
3120
3074
  /* -------- write helpers ---------- */
@@ -3244,94 +3198,185 @@ chc__write_string_body(chc_io *io, const uint64_t *offsets,
3244
3198
  return CHC_OK;
3245
3199
  }
3246
3200
 
3247
- /* Emit the entry's column body (no prefix). Assumes n_rows > 0. */
3201
+ /* -------- recursive column writer ---------- */
3202
+
3248
3203
  static int
3249
- chc__bld_write_body(chc_io *io, const chc__col_entry *e, chc_err *err)
3204
+ chc__col_write_mismatch(chc_err *err, const chc_type *t)
3250
3205
  {
3251
- int rc;
3252
- switch (e->kind) {
3253
- case CHC__BLD_FIXED:
3254
- if (e->fixed.elem_size)
3255
- return chc__write_bytes(io, e->fixed.data,
3256
- e->n_rows * e->fixed.elem_size, err);
3257
- return CHC_OK;
3206
+ size_t nl;
3207
+ const char *nm = chc_type_name(t, &nl);
3208
+ return chc__err_set(err, CHC_ERR_TYPE,
3209
+ "column layout does not match type %.*s", (int) nl, nm ? nm : "");
3210
+ }
3258
3211
 
3259
- case CHC__BLD_STRING:
3260
- case CHC__BLD_JSON_STRING:
3261
- return chc__write_string_body(io, e->str.offsets, e->str.data,
3262
- e->n_rows, err);
3212
+ /* Emit stream prefix before column bodies, mirroring chc__col_read_prefix:
3213
+ * LowCardinality key version 1, JSON serialization version 1, legacy Object
3214
+ * serialization kind 1. Recurse through composites */
3215
+ static int
3216
+ chc__col_write_prefix(chc_io *io, const chc_type *t, chc_err *err)
3217
+ {
3218
+ if (t->kind == CHC_LOW_CARDINALITY || t->kind == CHC_JSON)
3219
+ return chc__write_u64_le(io, 1, err);
3220
+ if (t->kind == CHC_OBJECT) {
3221
+ uint8_t kind = 1;
3222
+ return chc__write_bytes(io, &kind, 1, err);
3223
+ }
3224
+ if (t->kind == CHC_NULLABLE || t->kind == CHC_ARRAY
3225
+ || t->kind == CHC_TUPLE || t->kind == CHC_MAP
3226
+ || t->kind == CHC_NESTED
3227
+ || t->kind == CHC_SIMPLE_AGGREGATE_FUNCTION) {
3228
+ for (size_t i = 0; i < t->n_children; i++) {
3229
+ int rc = chc__col_write_prefix(io, t->children[i], err);
3230
+ if (rc != CHC_OK) return rc;
3231
+ }
3232
+ }
3233
+ return CHC_OK;
3234
+ }
3263
3235
 
3264
- case CHC__BLD_NULL_FIXED:
3265
- if ((rc = chc__write_bytes(io, e->nullable.null_map, e->n_rows, err))) return rc;
3266
- if (e->fixed.elem_size)
3267
- return chc__write_bytes(io, e->fixed.data,
3268
- e->n_rows * e->fixed.elem_size, err);
3236
+ /* Emit body from chc_column tree. Each node carries row count for its level.
3237
+ * Assume top-level block contains rows */
3238
+ static int
3239
+ chc__col_write(chc_io *io, const chc_column *c, const chc_type *t, chc_err *err)
3240
+ {
3241
+ int rc;
3242
+ size_t es = chc_type_elem_size(t);
3243
+ if (es) {
3244
+ if (c->layout != CHC_COL_FIXED) return chc__col_write_mismatch(err, t);
3245
+ if (c->n_rows)
3246
+ return chc__write_bytes(io, c->fixed.data, c->n_rows * es, err);
3269
3247
  return CHC_OK;
3248
+ }
3270
3249
 
3271
- case CHC__BLD_NULL_STRING:
3272
- if ((rc = chc__write_bytes(io, e->nullable.null_map, e->n_rows, err))) return rc;
3273
- return chc__write_string_body(io, e->str.offsets, e->str.data,
3274
- e->n_rows, err);
3275
-
3276
- case CHC__BLD_ARRAY_FIXED:
3277
- if ((rc = chc__write_u64_le_array(io, e->array.offsets, e->n_rows, err)))
3250
+ switch (t->kind) {
3251
+ case CHC_STRING:
3252
+ case CHC_JSON:
3253
+ case CHC_OBJECT:
3254
+ if (c->layout != CHC_COL_STRING) return chc__col_write_mismatch(err, t);
3255
+ return chc__write_string_body(io, c->str.offsets, c->str.data,
3256
+ c->n_rows, err);
3257
+
3258
+ case CHC_NULLABLE:
3259
+ if (c->layout != CHC_COL_NULLABLE || t->n_children != 1)
3260
+ return chc__col_write_mismatch(err, t);
3261
+ if (c->n_rows
3262
+ && (rc = chc__write_bytes(io, c->nullable.null_map, c->n_rows, err)))
3278
3263
  return rc;
3279
- if (e->inner_n && e->fixed.elem_size)
3280
- return chc__write_bytes(io, e->fixed.data,
3281
- e->inner_n * e->fixed.elem_size, err);
3282
- return CHC_OK;
3264
+ return chc__col_write(io, c->nullable.inner, t->children[0], err);
3283
3265
 
3284
- case CHC__BLD_ARRAY_STRING:
3285
- if ((rc = chc__write_u64_le_array(io, e->array.offsets, e->n_rows, err)))
3266
+ case CHC_ARRAY:
3267
+ if (c->layout != CHC_COL_ARRAY || t->n_children != 1)
3268
+ return chc__col_write_mismatch(err, t);
3269
+ if ((rc = chc__write_u64_le_array(io, c->array.offsets, c->n_rows, err)))
3270
+ return rc;
3271
+ return chc__col_write(io, c->array.values, t->children[0], err);
3272
+
3273
+ case CHC_MAP:
3274
+ case CHC_NESTED: {
3275
+ size_t arity = t->kind == CHC_MAP ? 2 : t->n_children;
3276
+ if (c->layout != CHC_COL_ARRAY || !arity || t->n_children != arity)
3277
+ return chc__col_write_mismatch(err, t);
3278
+ if ((rc = chc__write_u64_le_array(io, c->array.offsets, c->n_rows, err)))
3286
3279
  return rc;
3287
- return chc__write_string_body(io, e->str.offsets, e->str.data,
3288
- e->inner_n, err);
3280
+ const chc_column *tup = c->array.values;
3281
+ if (!tup || tup->layout != CHC_COL_TUPLE || tup->tuple.arity != arity)
3282
+ return chc__col_write_mismatch(err, t);
3283
+ for (size_t i = 0; i < arity; i++)
3284
+ if ((rc = chc__col_write(io, tup->tuple.children[i], t->children[i], err)))
3285
+ return rc;
3286
+ return CHC_OK;
3287
+ }
3289
3288
 
3290
- case CHC__BLD_ARRAY_NESTED_FIXED:
3291
- for (int lvl = 0; lvl < e->nested.ndim; lvl++) {
3292
- if ((rc = chc__write_u64_le_array(io, e->nested.level_offsets[lvl],
3293
- e->nested.level_offsets_len[lvl], err)))
3289
+ case CHC_TUPLE:
3290
+ if (c->layout != CHC_COL_TUPLE || c->tuple.arity != t->n_children)
3291
+ return chc__col_write_mismatch(err, t);
3292
+ for (size_t i = 0; i < t->n_children; i++)
3293
+ if ((rc = chc__col_write(io, c->tuple.children[i], t->children[i], err)))
3294
3294
  return rc;
3295
- }
3296
- if (e->inner_n && e->fixed.elem_size)
3297
- return chc__write_bytes(io, e->fixed.data,
3298
- e->inner_n * e->fixed.elem_size, err);
3299
3295
  return CHC_OK;
3300
3296
 
3301
- case CHC__BLD_ARRAY_NESTED_STRING:
3302
- for (int lvl = 0; lvl < e->nested.ndim; lvl++) {
3303
- if ((rc = chc__write_u64_le_array(io, e->nested.level_offsets[lvl],
3304
- e->nested.level_offsets_len[lvl], err)))
3297
+ case CHC_QBIT: {
3298
+ size_t bits = chc_type_qbit_element_size(t);
3299
+ if (!bits || t->n_children != 1
3300
+ || c->layout != CHC_COL_TUPLE || c->tuple.arity != bits)
3301
+ return chc__col_write_mismatch(err, t);
3302
+ size_t bytes_per_plane = (chc_type_qbit_dimension(t) + 7) / 8;
3303
+ for (size_t i = 0; i < bits; i++) {
3304
+ const chc_column *pl = c->tuple.children[i];
3305
+ if (pl->layout != CHC_COL_FIXED) return chc__col_write_mismatch(err, t);
3306
+ if (c->n_rows && (rc = chc__write_bytes(io, pl->fixed.data,
3307
+ c->n_rows * bytes_per_plane, err)))
3305
3308
  return rc;
3306
3309
  }
3307
- return chc__write_string_body(io, e->str.offsets, e->str.data,
3308
- e->inner_n, err);
3309
-
3310
- case CHC__BLD_LC_STRING: {
3311
- uint64_t flags = 0;
3312
- switch (e->lc.key_size) {
3313
- case 1: flags |= 0; break;
3314
- case 2: flags |= 1; break;
3315
- case 4: flags |= 2; break;
3316
- case 8: flags |= 3; break;
3310
+ return CHC_OK;
3311
+ }
3312
+
3313
+ case CHC_LOW_CARDINALITY: {
3314
+ if (c->layout != CHC_COL_LOW_CARDINALITY || t->n_children != 1)
3315
+ return chc__col_write_mismatch(err, t);
3316
+ /* Empty LC, including one under empty Array, has no body */
3317
+ if (c->n_rows == 0) return CHC_OK;
3318
+ const chc_type *inner = t->children[0];
3319
+ const chc_type *dict_type = (inner->kind == CHC_NULLABLE
3320
+ && inner->n_children == 1)
3321
+ ? inner->children[0] : inner;
3322
+ /* Reader wraps LC(Nullable) dict in Nullable for caller dispatch. Wire
3323
+ * body contains inner-typed strings with null sentinel at slot 0.
3324
+ * Unwrap before writing */
3325
+ const chc_column *dictc = c->lc.dict;
3326
+ if (dictc && dictc->layout == CHC_COL_NULLABLE)
3327
+ dictc = dictc->nullable.inner;
3328
+ uint64_t flags;
3329
+ switch (c->lc.key_size) {
3330
+ case 1: flags = 0; break;
3331
+ case 2: flags = 1; break;
3332
+ case 4: flags = 2; break;
3333
+ case 8: flags = 3; break;
3334
+ default: return chc__err_set(err, CHC_ERR_USAGE,
3335
+ "LowCardinality: bad key_size %d", c->lc.key_size);
3317
3336
  }
3318
3337
  flags |= CHC__LC_HAS_ADDITIONAL_KEYS;
3319
3338
  flags |= CHC__LC_NEED_UPDATE_DICT;
3320
3339
  if ((rc = chc__write_u64_le(io, flags, err))) return rc;
3321
- if ((rc = chc__write_u64_le(io, (uint64_t) e->inner_n, err))) return rc;
3322
- if ((rc = chc__write_string_body(io, e->str.offsets, e->str.data,
3323
- e->inner_n, err))) return rc;
3324
- if ((rc = chc__write_u64_le(io, (uint64_t) e->n_rows, err))) return rc;
3325
- return chc__write_keys_array(io, e->lc.keys, e->n_rows,
3326
- e->lc.key_size, err);
3340
+ if ((rc = chc__write_u64_le(io, (uint64_t) c->lc.dict_n, err))) return rc;
3341
+ if ((rc = chc__col_write(io, dictc, dict_type, err))) return rc;
3342
+ if ((rc = chc__write_u64_le(io, (uint64_t) c->n_rows, err))) return rc;
3343
+ return chc__write_keys_array(io, c->lc.keys, c->n_rows,
3344
+ c->lc.key_size, err);
3345
+ }
3346
+
3347
+ case CHC_SIMPLE_AGGREGATE_FUNCTION:
3348
+ if (t->n_children < 1) return chc__col_write_mismatch(err, t);
3349
+ return chc__col_write(io, c, t->children[0], err);
3350
+
3351
+ case CHC_POINT:
3352
+ case CHC_RING:
3353
+ case CHC_LINE_STRING:
3354
+ case CHC_POLYGON:
3355
+ case CHC_MULTI_LINE_STRING:
3356
+ case CHC_MULTI_POLYGON:
3357
+ return chc__col_write(io, c, chc__geo_alias(t), err);
3358
+
3359
+ case CHC_NOTHING:
3360
+ case CHC_VOID: {
3361
+ uint8_t zeros[256] = {};
3362
+ size_t left = c->n_rows;
3363
+ while (left) {
3364
+ size_t take = left < sizeof zeros ? left : sizeof zeros;
3365
+ if ((rc = chc__write_bytes(io, zeros, take, err))) return rc;
3366
+ left -= take;
3367
+ }
3368
+ return CHC_OK;
3327
3369
  }
3370
+
3371
+ default:
3372
+ return chc__col_write_mismatch(err, t);
3328
3373
  }
3329
- return chc__err_set(err, CHC_ERR_USAGE, "unknown builder kind %d", e->kind);
3330
3374
  }
3331
3375
 
3332
3376
  int
3333
- chc_block_write(chc_io *io, const chc_block_builder *bb,
3334
- const chc_block_opts *opts, chc_err *err)
3377
+ chc_block_write_cols(chc_io *io, const chc_block_col *cols,
3378
+ size_t n_cols, size_t n_rows,
3379
+ const chc_block_opts *opts, chc_err *err)
3335
3380
  {
3336
3381
  chc_block_opts def = {};
3337
3382
  if (!opts) opts = &def;
@@ -3341,28 +3386,30 @@ chc_block_write(chc_io *io, const chc_block_builder *bb,
3341
3386
  if (rc != CHC_OK) return rc;
3342
3387
  }
3343
3388
 
3344
- size_t n_rows = bb->n_rows_set ? bb->n_rows : 0;
3345
- int rc = chc__write_varuint(io, (uint64_t) bb->n_cols, err);
3389
+ int rc = chc__write_varuint(io, (uint64_t) n_cols, err);
3346
3390
  if (rc != CHC_OK) return rc;
3347
3391
  rc = chc__write_varuint(io, (uint64_t) n_rows, err);
3348
3392
  if (rc != CHC_OK) return rc;
3349
3393
 
3350
- for (size_t i = 0; i < bb->n_cols; i++) {
3351
- const chc__col_entry *e = &bb->cols[i];
3394
+ for (size_t i = 0; i < n_cols; i++) {
3395
+ const chc_block_col *e = &cols[i];
3396
+ if (!e->type || !e->col)
3397
+ return chc__err_set(err, CHC_ERR_USAGE,
3398
+ "block_write: column %zu has NULL type or tree", i);
3399
+ if (e->col->n_rows != n_rows)
3400
+ return chc__err_set(err, CHC_ERR_USAGE,
3401
+ "block_write: column %zu row count %zu != block %zu",
3402
+ i, e->col->n_rows, n_rows);
3403
+
3352
3404
  rc = chc__write_string(io, e->name, e->name_len, err);
3353
3405
  if (rc != CHC_OK) return rc;
3354
3406
 
3355
- /* Type name: legacy STRING path has no e->type; emit "String". */
3356
- if (e->kind == CHC__BLD_STRING && !e->type) {
3357
- rc = chc__write_string(io, "String", 6, err);
3358
- } else {
3359
- char tbuf[256];
3360
- size_t need = chc_type_format(e->type, tbuf, sizeof tbuf);
3361
- if (need >= sizeof tbuf)
3362
- return chc__err_set(err, CHC_ERR_USAGE,
3363
- "type name too long for inline buffer");
3364
- rc = chc__write_string(io, tbuf, need, err);
3365
- }
3407
+ char tbuf[256];
3408
+ size_t need = chc_type_format(e->type, tbuf, sizeof tbuf);
3409
+ if (need >= sizeof tbuf)
3410
+ return chc__err_set(err, CHC_ERR_USAGE,
3411
+ "type name too long for inline buffer");
3412
+ rc = chc__write_string(io, tbuf, need, err);
3366
3413
  if (rc != CHC_OK) return rc;
3367
3414
 
3368
3415
  if (opts->has_custom_serialization) {
@@ -3371,20 +3418,24 @@ chc_block_write(chc_io *io, const chc_block_builder *bb,
3371
3418
  if (rc != CHC_OK) return rc;
3372
3419
  }
3373
3420
 
3374
- if (e->n_rows == 0) continue;
3375
-
3376
- /* LC and JSON prefixes use version 1. */
3377
- if (e->kind == CHC__BLD_LC_STRING || e->kind == CHC__BLD_JSON_STRING) {
3378
- rc = chc__write_u64_le(io, 1, err);
3379
- if (rc != CHC_OK) return rc;
3380
- }
3421
+ if (n_rows == 0) continue;
3381
3422
 
3382
- rc = chc__bld_write_body(io, e, err);
3423
+ rc = chc__col_write_prefix(io, e->type, err);
3424
+ if (rc != CHC_OK) return rc;
3425
+ rc = chc__col_write(io, e->col, e->type, err);
3383
3426
  if (rc != CHC_OK) return rc;
3384
3427
  }
3385
3428
  return CHC_OK;
3386
3429
  }
3387
3430
 
3431
+ int
3432
+ chc_block_write(chc_io *io, const chc_block_builder *bb,
3433
+ const chc_block_opts *opts, chc_err *err)
3434
+ {
3435
+ return chc_block_write_cols(io, bb->cols, bb->n_cols,
3436
+ bb->n_rows, opts, err);
3437
+ }
3438
+
3388
3439
  #endif /* CHC_IMPLEMENTATION */
3389
3440
 
3390
3441
  #ifdef __cplusplus