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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +8 -8
- data/ext/ch_connect_native/ch_connect_native.c +196 -49
- data/lib/ch_connect/connection.rb +19 -1
- data/lib/ch_connect/response.rb +12 -1
- data/lib/ch_connect/version.rb +1 -1
- data/vendor/clickhouse-c/VENDOR.md +5 -2
- data/vendor/clickhouse-c/clickhouse-async.h +15 -22
- data/vendor/clickhouse-c/clickhouse-client.h +187 -212
- data/vendor/clickhouse-c/clickhouse-compression.h +8 -5
- data/vendor/clickhouse-c/clickhouse.h +1033 -982
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88e326f103cde2be1cd0ed39c0615ff2861020fb2f5d334fcb08ff012196cd3b
|
|
4
|
+
data.tar.gz: 0c6b12e11fdfec5626fefa1cbbcf81da8ed384f4983d1caf0466758adcb617db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fda2b6e230f5a66d66a0cd4189dc324f76713756c458bd67e06f1dc0873ec589c53980e497eaa9d37a5dd985ba0f8ef60264bafe8750b2d331ce33e93d12b574
|
|
7
|
+
data.tar.gz: eaa8b14dd4da0af03c2c3ab66825ab27c4770d0f7a7dd596deb4daa06a6b0fc3191a9454b4bd0f3411b3cbe1f85dd5eaf1ff2546a2e20c9160d7f94ae1572b43
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.4.1] - 2026-10-12
|
|
2
|
+
|
|
3
|
+
- Improved large result decoding speed (about 1.9x faster for the 100K-row benchmark query) and reduced its allocations from 97 MB to 55 MB
|
|
4
|
+
- Fixed quadratic re-parsing of large uncompressed result blocks (about 5x faster for multi-megabyte String columns)
|
|
5
|
+
- Faster decoding of repeated `Date`/`Date32` values, `Nullable(String)`, `UUID`, and `Map` with String keys, and faster `Response#each`
|
|
6
|
+
|
|
7
|
+
## [0.4.0] - 2026-10-06
|
|
8
|
+
|
|
9
|
+
- Raised the minimum supported ClickHouse version to 23.3; connections to older servers are rejected during the handshake
|
|
10
|
+
- Updated vendored clickhouse-c headers to `916cbf5`, adding sparse column decoding
|
|
11
|
+
- Improved decimal decoding performance while preserving exact values across all decimal widths
|
|
12
|
+
- Fixed decoding of geometry types, including Point, Ring, Polygon, MultiPolygon, LineString, and MultiLineString
|
|
13
|
+
- Preserved complete UTF-8 server error messages during connection establishment
|
|
14
|
+
|
|
1
15
|
## [0.3.1] - 2026-08-19
|
|
2
16
|
|
|
3
17
|
- Fixed LZ4 compression negotiation for ClickHouse users configured with `readonly = 1`
|
data/README.md
CHANGED
|
@@ -22,27 +22,27 @@ Speed (iterations/second, higher is better):
|
|
|
22
22
|
|
|
23
23
|
| Scenario | ch_connect | click_house | clickhouse | click_house-client |
|
|
24
24
|
|----------|------------|-------------|------------|--------------------|
|
|
25
|
-
| Small queries (10 rows) | **
|
|
26
|
-
| Large queries (100K rows) | **
|
|
25
|
+
| Small queries (10 rows) | **1961 i/s** | 718 i/s (2.7x slower) | 524 i/s (3.7x slower) | 796 i/s (2.5x slower) |
|
|
26
|
+
| Large queries (100K rows) | **14.2 i/s** | 1.4 i/s (10.5x slower) | 0.5 i/s (26.4x slower) | 1.8 i/s (8.0x slower) |
|
|
27
27
|
|
|
28
28
|
Multi-threaded throughput (10K-row query, total queries/second):
|
|
29
29
|
|
|
30
30
|
| Threads | ch_connect | click_house | clickhouse | click_house-client |
|
|
31
31
|
|---------|------------|-------------|------------|--------------------|
|
|
32
|
-
| 1 | **
|
|
33
|
-
| 4 | **
|
|
34
|
-
| 8 | **
|
|
32
|
+
| 1 | **523 q/s** | 74 q/s | 140 q/s | 56 q/s |
|
|
33
|
+
| 4 | **755 q/s** | 148 q/s | 306 q/s | 78 q/s |
|
|
34
|
+
| 8 | **637 q/s** | 150 q/s | 326 q/s | 82 q/s |
|
|
35
35
|
|
|
36
36
|
Memory allocated for a large query (lower is better):
|
|
37
37
|
|
|
38
38
|
| Gem | Allocated |
|
|
39
39
|
|-----|-----------|
|
|
40
|
-
| ch_connect | **
|
|
40
|
+
| ch_connect | **55 MB** |
|
|
41
41
|
| click_house | 198 MB |
|
|
42
42
|
| click_house-client | 210 MB |
|
|
43
43
|
| clickhouse | 436 MB |
|
|
44
44
|
|
|
45
|
-
A small query allocates approximately **3.
|
|
45
|
+
A small query allocates approximately **3.6 KB / 40 Ruby objects**. See the [`benchmark/`](benchmark/) directory for the comparison suite and methodology.
|
|
46
46
|
|
|
47
47
|
## Installation
|
|
48
48
|
|
|
@@ -52,7 +52,7 @@ Add the gem to your Gemfile:
|
|
|
52
52
|
gem "ch_connect"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The gem contains a native extension and
|
|
55
|
+
The gem contains a native extension and requires MRI Ruby and ClickHouse 23.3 or newer. LZ4 and ZSTD support are detected when the extension is built.
|
|
56
56
|
|
|
57
57
|
## Configuration
|
|
58
58
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
#include <ruby/encoding.h>
|
|
17
17
|
#include <ruby/intern.h>
|
|
18
18
|
|
|
19
|
+
#include <inttypes.h>
|
|
19
20
|
#include <limits.h>
|
|
20
21
|
#include <string.h>
|
|
21
22
|
#include <time.h>
|
|
@@ -40,8 +41,6 @@ static VALUE vAF_INET6;
|
|
|
40
41
|
|
|
41
42
|
static ID id_jd;
|
|
42
43
|
static ID id_new;
|
|
43
|
-
static ID id_pow;
|
|
44
|
-
static ID id_div;
|
|
45
44
|
static ID id_BigDecimal;
|
|
46
45
|
|
|
47
46
|
static VALUE sym_read_rows;
|
|
@@ -59,6 +58,9 @@ static VALUE sym_zstd;
|
|
|
59
58
|
/* Julian day number of 1970-01-01 (Date.jd(2440588) == Date.new(1970, 1, 1)) */
|
|
60
59
|
#define UNIX_EPOCH_JD 2440588
|
|
61
60
|
|
|
61
|
+
/* Per-column Date cache slots; must be a power of two */
|
|
62
|
+
#define DATE_CACHE_SIZE 256
|
|
63
|
+
|
|
62
64
|
/* One immutable codec shared by all connections and threads: the built-in
|
|
63
65
|
* adapters are stateless wrappers over the one-shot lz4/zstd functions. Both
|
|
64
66
|
* slot pairs are filled (when available) so decode survives a server-side
|
|
@@ -136,6 +138,39 @@ load_i64le(const uint8_t *p)
|
|
|
136
138
|
return v;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/* Little-endian two's complement value of 4, 8, 16 or 32 bytes as int64, when it fits. */
|
|
142
|
+
static int
|
|
143
|
+
load_decimal_i64(const uint8_t *p, size_t nbytes, int64_t *out)
|
|
144
|
+
{
|
|
145
|
+
if (nbytes == 4) {
|
|
146
|
+
*out = load_i32le(p);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
int64_t v = load_i64le(p);
|
|
150
|
+
uint8_t sign = v < 0 ? 0xFF : 0x00;
|
|
151
|
+
for (size_t b = 8; b < nbytes; b++) {
|
|
152
|
+
if (p[b] != sign) return 0;
|
|
153
|
+
}
|
|
154
|
+
*out = v;
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* "<unscaled>e-<scale>" for a decimal value. BigDecimal parses it exactly,
|
|
159
|
+
* without the intermediate BigDecimals of dividing by 10**scale. Only values
|
|
160
|
+
* outside the int64 range go through a Ruby Integer. */
|
|
161
|
+
static VALUE
|
|
162
|
+
decimal_literal(const uint8_t *p, size_t nbytes, int scale)
|
|
163
|
+
{
|
|
164
|
+
int64_t v;
|
|
165
|
+
if (load_decimal_i64(p, nbytes, &v)) {
|
|
166
|
+
char buf[48];
|
|
167
|
+
int len = snprintf(buf, sizeof(buf), "%" PRId64 "e-%d", v, scale);
|
|
168
|
+
return rb_usascii_str_new(buf, len);
|
|
169
|
+
}
|
|
170
|
+
VALUE unscaled = rb_integer_unpack(p, nbytes, 1, 0, INTEGER_PACK_LITTLE_ENDIAN | INTEGER_PACK_2COMP);
|
|
171
|
+
return rb_str_catf(rb_big2str(unscaled, 10), "e-%d", scale);
|
|
172
|
+
}
|
|
173
|
+
|
|
139
174
|
static inline float
|
|
140
175
|
load_f32le(const uint8_t *p)
|
|
141
176
|
{
|
|
@@ -259,17 +294,23 @@ decode_fixed(const chc_column *col, const chc_type *t, long n_rows, native_state
|
|
|
259
294
|
}
|
|
260
295
|
break;
|
|
261
296
|
}
|
|
262
|
-
case CHC_DATE:
|
|
263
|
-
for (long i = 0; i < n_rows; i++) {
|
|
264
|
-
uint16_t days = load_u16le(data + i * 2);
|
|
265
|
-
rb_ary_push(ary, rb_funcall(cDate, id_jd, 1, LONG2NUM(UNIX_EPOCH_JD + (long)days)));
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
297
|
+
case CHC_DATE:
|
|
269
298
|
case CHC_DATE32: {
|
|
299
|
+
/* Date construction is a Ruby method call and columns usually repeat
|
|
300
|
+
* a handful of days, so reuse (immutable) Dates through a small
|
|
301
|
+
* direct-mapped cache. Every cached Date is already anchored by ary. */
|
|
302
|
+
long cache_days[DATE_CACHE_SIZE];
|
|
303
|
+
VALUE cache_vals[DATE_CACHE_SIZE];
|
|
304
|
+
for (int k = 0; k < DATE_CACHE_SIZE; k++) cache_vals[k] = Qundef;
|
|
270
305
|
for (long i = 0; i < n_rows; i++) {
|
|
271
|
-
|
|
272
|
-
|
|
306
|
+
long days = kind == CHC_DATE ? (long)load_u16le(data + i * 2)
|
|
307
|
+
: (long)load_i32le(data + i * 4);
|
|
308
|
+
unsigned long slot = (unsigned long)days & (DATE_CACHE_SIZE - 1);
|
|
309
|
+
if (cache_vals[slot] == Qundef || cache_days[slot] != days) {
|
|
310
|
+
cache_days[slot] = days;
|
|
311
|
+
cache_vals[slot] = rb_funcall(cDate, id_jd, 1, LONG2NUM(UNIX_EPOCH_JD + days));
|
|
312
|
+
}
|
|
313
|
+
rb_ary_push(ary, cache_vals[slot]);
|
|
273
314
|
}
|
|
274
315
|
break;
|
|
275
316
|
}
|
|
@@ -299,18 +340,23 @@ decode_fixed(const chc_column *col, const chc_type *t, long n_rows, native_state
|
|
|
299
340
|
break;
|
|
300
341
|
}
|
|
301
342
|
case CHC_UUID: {
|
|
302
|
-
|
|
343
|
+
/* Each 8-byte half is little-endian on the wire; emit both halves
|
|
344
|
+
* most significant nibble first, with dashes after hex digits
|
|
345
|
+
* 8, 12, 16 and 20. */
|
|
346
|
+
static const char hex[] = "0123456789abcdef";
|
|
303
347
|
for (long i = 0; i < n_rows; i++) {
|
|
304
348
|
const uint8_t *p = data + i * 16;
|
|
305
|
-
uint64_t
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
349
|
+
uint64_t halves[2] = { load_u64le(p), load_u64le(p + 8) };
|
|
350
|
+
VALUE str = rb_utf8_str_new(NULL, 36);
|
|
351
|
+
char *out = RSTRING_PTR(str);
|
|
352
|
+
int o = 0;
|
|
353
|
+
for (int h = 0; h < 2; h++) {
|
|
354
|
+
for (int shift = 60; shift >= 0; shift -= 4) {
|
|
355
|
+
if (o == 8 || o == 13 || o == 18 || o == 23) out[o++] = '-';
|
|
356
|
+
out[o++] = hex[(halves[h] >> shift) & 0xF];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
rb_ary_push(ary, str);
|
|
314
360
|
}
|
|
315
361
|
break;
|
|
316
362
|
}
|
|
@@ -334,23 +380,9 @@ decode_fixed(const chc_column *col, const chc_type *t, long n_rows, native_state
|
|
|
334
380
|
case CHC_DECIMAL128:
|
|
335
381
|
case CHC_DECIMAL256: {
|
|
336
382
|
int scale = chc_type_decimal_scale(t);
|
|
337
|
-
|
|
383
|
+
size_t nbytes = kind == CHC_DECIMAL32 ? 4 : kind == CHC_DECIMAL64 ? 8 : kind == CHC_DECIMAL128 ? 16 : 32;
|
|
338
384
|
for (long i = 0; i < n_rows; i++) {
|
|
339
|
-
|
|
340
|
-
if (kind == CHC_DECIMAL32) {
|
|
341
|
-
int32_t v = load_i32le(data + i * 4);
|
|
342
|
-
unscaled = INT2NUM(v);
|
|
343
|
-
} else if (kind == CHC_DECIMAL64) {
|
|
344
|
-
int64_t v = load_i64le(data + i * 8);
|
|
345
|
-
unscaled = LL2NUM(v);
|
|
346
|
-
} else {
|
|
347
|
-
size_t nbytes = (kind == CHC_DECIMAL128) ? 16 : 32;
|
|
348
|
-
unscaled = rb_integer_unpack(data + i * nbytes, nbytes, 1, 0,
|
|
349
|
-
INTEGER_PACK_LITTLE_ENDIAN |
|
|
350
|
-
INTEGER_PACK_2COMP);
|
|
351
|
-
}
|
|
352
|
-
VALUE bd = rb_funcall(rb_mKernel, id_BigDecimal, 1, unscaled);
|
|
353
|
-
rb_ary_push(ary, rb_funcall(bd, id_div, 1, divisor));
|
|
385
|
+
rb_ary_push(ary, rb_funcall(rb_mKernel, id_BigDecimal, 1, decimal_literal(data + i * nbytes, nbytes, scale)));
|
|
354
386
|
}
|
|
355
387
|
break;
|
|
356
388
|
}
|
|
@@ -361,17 +393,98 @@ decode_fixed(const chc_column *col, const chc_type *t, long n_rows, native_state
|
|
|
361
393
|
return ary;
|
|
362
394
|
}
|
|
363
395
|
|
|
396
|
+
/* null_map (optional) marks rows that a Nullable wrapper turns into nil, so
|
|
397
|
+
* their placeholder strings are never allocated. */
|
|
398
|
+
static VALUE
|
|
399
|
+
decode_string_column(const chc_column *col, long n_rows, const uint8_t *null_map)
|
|
400
|
+
{
|
|
401
|
+
const uint8_t *data = chc_column_string_data(col);
|
|
402
|
+
const uint64_t *offsets = chc_column_string_offsets(col);
|
|
403
|
+
VALUE ary = rb_ary_new_capa(n_rows);
|
|
404
|
+
|
|
405
|
+
uint64_t start = 0;
|
|
406
|
+
for (long i = 0; i < n_rows; i++) {
|
|
407
|
+
uint64_t end = offsets[i];
|
|
408
|
+
if (null_map && null_map[i] == 1) rb_ary_push(ary, Qnil);
|
|
409
|
+
else rb_ary_push(ary, rb_utf8_str_new((const char *)data + start, (long)(end - start)));
|
|
410
|
+
start = end;
|
|
411
|
+
}
|
|
412
|
+
return ary;
|
|
413
|
+
}
|
|
414
|
+
|
|
364
415
|
static VALUE
|
|
365
|
-
|
|
416
|
+
decode_interned_string_column(const chc_column *col, long n_rows)
|
|
366
417
|
{
|
|
367
418
|
const uint8_t *data = chc_column_string_data(col);
|
|
368
419
|
const uint64_t *offsets = chc_column_string_offsets(col);
|
|
420
|
+
rb_encoding *utf8 = rb_utf8_encoding();
|
|
369
421
|
VALUE ary = rb_ary_new_capa(n_rows);
|
|
370
422
|
|
|
371
423
|
uint64_t start = 0;
|
|
372
424
|
for (long i = 0; i < n_rows; i++) {
|
|
373
425
|
uint64_t end = offsets[i];
|
|
374
|
-
rb_ary_push(ary,
|
|
426
|
+
rb_ary_push(ary, rb_enc_interned_str((const char *)data + start, (long)(end - start), utf8));
|
|
427
|
+
start = end;
|
|
428
|
+
}
|
|
429
|
+
return ary;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static void
|
|
433
|
+
raise_invalid_geometry(native_state *state, const char *detail)
|
|
434
|
+
{
|
|
435
|
+
*state = NATIVE_BROKEN;
|
|
436
|
+
rb_raise(eConnectionError, "Invalid native geometry column: %s", detail);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* clickhouse-c exposes geometry aliases as their wire-level Array/Tuple
|
|
440
|
+
* column tree, while the logical geometry type has no synthetic children. */
|
|
441
|
+
static VALUE
|
|
442
|
+
decode_geometry(const chc_column *col, long n_rows, int depth, native_state *state)
|
|
443
|
+
{
|
|
444
|
+
if (n_rows == 0) return rb_ary_new();
|
|
445
|
+
|
|
446
|
+
if (depth == 0) {
|
|
447
|
+
if (chc_column_layout(col) != CHC_COL_TUPLE ||
|
|
448
|
+
chc_column_tuple_arity(col) != 2) {
|
|
449
|
+
raise_invalid_geometry(state, "Point must be a two-coordinate tuple");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const chc_column *x_col = chc_column_tuple_child(col, 0);
|
|
453
|
+
const chc_column *y_col = chc_column_tuple_child(col, 1);
|
|
454
|
+
size_t x_size = 0;
|
|
455
|
+
size_t y_size = 0;
|
|
456
|
+
const uint8_t *x = (const uint8_t *)chc_column_fixed_data(x_col, &x_size);
|
|
457
|
+
const uint8_t *y = (const uint8_t *)chc_column_fixed_data(y_col, &y_size);
|
|
458
|
+
if (!x || !y || x_size != 8 || y_size != 8) {
|
|
459
|
+
raise_invalid_geometry(state, "Point coordinates must be Float64 columns");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
VALUE points = rb_ary_new_capa(n_rows);
|
|
463
|
+
for (long i = 0; i < n_rows; i++) {
|
|
464
|
+
VALUE point = rb_ary_new_capa(2);
|
|
465
|
+
rb_ary_push(point, DBL2NUM(load_f64le(x + i * 8)));
|
|
466
|
+
rb_ary_push(point, DBL2NUM(load_f64le(y + i * 8)));
|
|
467
|
+
rb_ary_push(points, point);
|
|
468
|
+
}
|
|
469
|
+
return points;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (chc_column_layout(col) != CHC_COL_ARRAY) {
|
|
473
|
+
raise_invalid_geometry(state, "expected an Array layer");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const uint64_t *offsets = chc_column_array_offsets(col);
|
|
477
|
+
const chc_column *values_col = chc_column_array_values(col);
|
|
478
|
+
long total = (long)chc_column_n_rows(values_col);
|
|
479
|
+
VALUE elements = decode_geometry(values_col, total, depth - 1, state);
|
|
480
|
+
VALUE ary = rb_ary_new_capa(n_rows);
|
|
481
|
+
uint64_t start = 0;
|
|
482
|
+
for (long i = 0; i < n_rows; i++) {
|
|
483
|
+
uint64_t end = offsets[i];
|
|
484
|
+
if (end < start || end > (uint64_t)RARRAY_LEN(elements)) {
|
|
485
|
+
raise_invalid_geometry(state, "Array offset is out of bounds");
|
|
486
|
+
}
|
|
487
|
+
rb_ary_push(ary, rb_ary_subseq(elements, (long)start, (long)(end - start)));
|
|
375
488
|
start = end;
|
|
376
489
|
}
|
|
377
490
|
return ary;
|
|
@@ -380,6 +493,21 @@ decode_string_column(const chc_column *col, long n_rows)
|
|
|
380
493
|
static VALUE
|
|
381
494
|
decode_column(const chc_column *col, const chc_type *t, long n_rows, native_state *state)
|
|
382
495
|
{
|
|
496
|
+
switch (chc_type_kind(t)) {
|
|
497
|
+
case CHC_POINT:
|
|
498
|
+
return decode_geometry(col, n_rows, 0, state);
|
|
499
|
+
case CHC_RING:
|
|
500
|
+
case CHC_LINE_STRING:
|
|
501
|
+
return decode_geometry(col, n_rows, 1, state);
|
|
502
|
+
case CHC_POLYGON:
|
|
503
|
+
case CHC_MULTI_LINE_STRING:
|
|
504
|
+
return decode_geometry(col, n_rows, 2, state);
|
|
505
|
+
case CHC_MULTI_POLYGON:
|
|
506
|
+
return decode_geometry(col, n_rows, 3, state);
|
|
507
|
+
default:
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
383
511
|
if (n_rows == 0) {
|
|
384
512
|
chc_kind kind = chc_type_kind(t);
|
|
385
513
|
if (kind == CHC_STRING) return rb_ary_new();
|
|
@@ -398,12 +526,14 @@ decode_column(const chc_column *col, const chc_type *t, long n_rows, native_stat
|
|
|
398
526
|
return decode_fixed(col, t, n_rows, state);
|
|
399
527
|
|
|
400
528
|
case CHC_COL_STRING:
|
|
401
|
-
return decode_string_column(col, n_rows);
|
|
529
|
+
return decode_string_column(col, n_rows, NULL);
|
|
402
530
|
|
|
403
531
|
case CHC_COL_NULLABLE: {
|
|
404
532
|
const uint8_t *null_map = chc_column_null_map(col);
|
|
405
533
|
const chc_column *inner = chc_column_nullable_inner(col);
|
|
406
534
|
const chc_type *inner_t = chc_type_child(t, 0);
|
|
535
|
+
if (chc_column_layout(inner) == CHC_COL_STRING)
|
|
536
|
+
return decode_string_column(inner, n_rows, null_map);
|
|
407
537
|
VALUE vals = decode_column(inner, inner_t, n_rows, state);
|
|
408
538
|
for (long i = 0; i < n_rows; i++) {
|
|
409
539
|
if (null_map[i] == 1) rb_ary_store(vals, i, Qnil);
|
|
@@ -422,7 +552,12 @@ decode_column(const chc_column *col, const chc_type *t, long n_rows, native_stat
|
|
|
422
552
|
long total = (long)chc_column_n_rows(values_col);
|
|
423
553
|
const chc_column *keys_col = chc_column_tuple_child(values_col, 0);
|
|
424
554
|
const chc_column *vals_col = chc_column_tuple_child(values_col, 1);
|
|
425
|
-
|
|
555
|
+
/* Hash#[]= dups and freezes unfrozen String keys; decoding them
|
|
556
|
+
* as interned (frozen, deduplicated) strings skips that copy, and
|
|
557
|
+
* map keys typically repeat across rows. */
|
|
558
|
+
VALUE keys = chc_column_layout(keys_col) == CHC_COL_STRING
|
|
559
|
+
? decode_interned_string_column(keys_col, total)
|
|
560
|
+
: decode_column(keys_col, kt, total, state);
|
|
426
561
|
VALUE vals = decode_column(vals_col, vt, total, state);
|
|
427
562
|
|
|
428
563
|
VALUE ary = rb_ary_new_capa(n_rows);
|
|
@@ -435,7 +570,7 @@ decode_column(const chc_column *col, const chc_type *t, long n_rows, native_stat
|
|
|
435
570
|
"Map offset out of bounds at row %ld: %llu", i,
|
|
436
571
|
(unsigned long long)end);
|
|
437
572
|
}
|
|
438
|
-
VALUE hash =
|
|
573
|
+
VALUE hash = rb_hash_new_capa((long)(end - start));
|
|
439
574
|
for (uint64_t j = start; j < end; j++) {
|
|
440
575
|
rb_hash_aset(hash, RARRAY_AREF(keys, (long)j), RARRAY_AREF(vals, (long)j));
|
|
441
576
|
}
|
|
@@ -638,7 +773,8 @@ native_client_handshake_step(VALUE self)
|
|
|
638
773
|
|
|
639
774
|
nc->state = NATIVE_ACTIVE;
|
|
640
775
|
chc_err err = {0};
|
|
641
|
-
|
|
776
|
+
chc_exception *exception = NULL;
|
|
777
|
+
int rc = chc_async_handshake(nc->ac, &exception, &err);
|
|
642
778
|
if (rc == CHC_OK) {
|
|
643
779
|
nc->state = NATIVE_READY;
|
|
644
780
|
return sym_done;
|
|
@@ -646,6 +782,16 @@ native_client_handshake_step(VALUE self)
|
|
|
646
782
|
if (rc == CHC_WOULD_BLOCK) return sym_want_read;
|
|
647
783
|
|
|
648
784
|
nc->state = NATIVE_BROKEN;
|
|
785
|
+
if (exception) {
|
|
786
|
+
/* Anchor the server exception before allocating Ruby objects so close
|
|
787
|
+
* or GC can still release it if message construction raises. */
|
|
788
|
+
nc->pending_pkt.kind = CHC_PKT_EXCEPTION;
|
|
789
|
+
nc->pending_pkt.exception = exception;
|
|
790
|
+
VALUE msg = rb_utf8_str_new(exception->display_text,
|
|
791
|
+
(long)exception->display_text_len);
|
|
792
|
+
clear_pending(nc);
|
|
793
|
+
rb_exc_raise(rb_exc_new_str(eConnectionError, msg));
|
|
794
|
+
}
|
|
649
795
|
rb_raise(eConnectionError, "%s", err.msg);
|
|
650
796
|
}
|
|
651
797
|
|
|
@@ -808,13 +954,16 @@ native_client_decode_block(VALUE self, native_client_t *nc, chc_block *block)
|
|
|
808
954
|
(long)n_rows, &nc->state);
|
|
809
955
|
rb_ary_push(col_vals, cols[i]);
|
|
810
956
|
}
|
|
957
|
+
/* row_vals only holds references that cols[] already anchors */
|
|
958
|
+
VALUE row_buf;
|
|
959
|
+
VALUE *row_vals = ALLOCV_N(VALUE, row_buf, n_cols);
|
|
811
960
|
for (size_t r = 0; r < n_rows; r++) {
|
|
812
|
-
VALUE row = rb_ary_new_capa((long)n_cols);
|
|
813
961
|
for (size_t c = 0; c < n_cols; c++) {
|
|
814
|
-
|
|
962
|
+
row_vals[c] = RARRAY_AREF(cols[c], (long)r);
|
|
815
963
|
}
|
|
816
|
-
rb_ary_push(rows,
|
|
964
|
+
rb_ary_push(rows, rb_ary_new_from_values((long)n_cols, row_vals));
|
|
817
965
|
}
|
|
966
|
+
ALLOCV_END(row_buf);
|
|
818
967
|
ALLOCV_END(cols_buf);
|
|
819
968
|
RB_GC_GUARD(col_vals);
|
|
820
969
|
}
|
|
@@ -880,7 +1029,7 @@ native_client_recv_step(VALUE self)
|
|
|
880
1029
|
nc->state = NATIVE_READY;
|
|
881
1030
|
return sym_done;
|
|
882
1031
|
}
|
|
883
|
-
/* PONG / TOTALS / EXTREMES / LOG / PROFILE_EVENTS — skip. */
|
|
1032
|
+
/* PONG / TOTALS / EXTREMES / LOG / PROFILE_EVENTS / TIMEZONE_UPDATE — skip. */
|
|
884
1033
|
}
|
|
885
1034
|
}
|
|
886
1035
|
|
|
@@ -969,8 +1118,6 @@ Init_ch_connect_native(void)
|
|
|
969
1118
|
|
|
970
1119
|
id_jd = rb_intern("jd");
|
|
971
1120
|
id_new = rb_intern("new");
|
|
972
|
-
id_pow = rb_intern("**");
|
|
973
|
-
id_div = rb_intern("/");
|
|
974
1121
|
id_BigDecimal = rb_intern("BigDecimal");
|
|
975
1122
|
|
|
976
1123
|
sym_read_rows = ID2SYM(rb_intern("read_rows"));
|
|
@@ -27,6 +27,7 @@ module ChConnect
|
|
|
27
27
|
|
|
28
28
|
READ_CHUNK = 64 * 1024
|
|
29
29
|
WRITE_CHUNK = 64 * 1024
|
|
30
|
+
DRAIN_LIMIT = 16 * 1024 * 1024
|
|
30
31
|
PARAM_ESCAPES = {
|
|
31
32
|
"\0" => "\\0", "\a" => "\\a", "\b" => "\\b", "\e" => "\\e",
|
|
32
33
|
"\f" => "\\f", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t",
|
|
@@ -60,7 +61,7 @@ module ChConnect
|
|
|
60
61
|
loop do
|
|
61
62
|
case @client.recv_step
|
|
62
63
|
when :done then break
|
|
63
|
-
when :want_read then
|
|
64
|
+
when :want_read then feed_available
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
@client.take_result
|
|
@@ -191,6 +192,23 @@ module ChConnect
|
|
|
191
192
|
end
|
|
192
193
|
end
|
|
193
194
|
|
|
195
|
+
# Blocks for one chunk, then drains whatever else the socket already has
|
|
196
|
+
# buffered (up to DRAIN_LIMIT). An uncompressed Data block that ends
|
|
197
|
+
# mid-column is re-parsed from that column's start on the next
|
|
198
|
+
# recv_step, so retrying once per burst instead of once per chunk keeps
|
|
199
|
+
# large blocks from going quadratic.
|
|
200
|
+
def feed_available
|
|
201
|
+
@client.feed(read_chunk)
|
|
202
|
+
drained = 0
|
|
203
|
+
while drained < DRAIN_LIMIT
|
|
204
|
+
chunk = @socket.read_nonblock(READ_CHUNK, @read_buf, exception: false)
|
|
205
|
+
break unless chunk.is_a?(String)
|
|
206
|
+
|
|
207
|
+
@client.feed(chunk)
|
|
208
|
+
drained += chunk.bytesize
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
194
212
|
# Reads into a reusable buffer: feed copies the bytes synchronously,
|
|
195
213
|
# so the buffer never needs to survive past the next read.
|
|
196
214
|
def read_chunk(timeout = @config.read_timeout)
|
data/lib/ch_connect/response.rb
CHANGED
|
@@ -17,7 +17,18 @@ module ChConnect
|
|
|
17
17
|
def each
|
|
18
18
|
return to_enum(:each) unless block_given?
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# An index loop avoids zip's per-row pair arrays (~3x faster under YJIT).
|
|
21
|
+
names = columns
|
|
22
|
+
count = names.size
|
|
23
|
+
rows.each do |row|
|
|
24
|
+
hash = {}
|
|
25
|
+
index = 0
|
|
26
|
+
while index < count
|
|
27
|
+
hash[names[index]] = row[index]
|
|
28
|
+
index += 1
|
|
29
|
+
end
|
|
30
|
+
yield hash
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
end
|
data/lib/ch_connect/version.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Vendored: clickhouse-c
|
|
2
2
|
|
|
3
3
|
- Source: https://github.com/ClickHouse/clickhouse-c
|
|
4
|
-
- Commit:
|
|
4
|
+
- Commit: 916cbf5ff41a218978bc073bca10f5b609a00081 (updated 2026-10-05)
|
|
5
5
|
- License: Apache-2.0 (see LICENSE)
|
|
6
6
|
|
|
7
|
+
The native client requires ClickHouse 23.3 or newer and advertises protocol
|
|
8
|
+
revision 54465, including sparse column serialization support.
|
|
9
|
+
|
|
7
10
|
Only the headers the extension includes are vendored: `clickhouse.h`,
|
|
8
11
|
`clickhouse-compression.h`, `clickhouse-client.h`, `clickhouse-async.h`
|
|
9
12
|
(plus LICENSE). Upstream's tests, docs, tools, and the `clickhouse-posix-io.h`
|
|
@@ -12,7 +15,7 @@ ioless (Ruby owns the socket and TLS).
|
|
|
12
15
|
|
|
13
16
|
## Local patches
|
|
14
17
|
|
|
15
|
-
- `clickhouse.h` — `
|
|
18
|
+
- `clickhouse.h` — `chc__col_read_tuple`: ClickHouse serializes the
|
|
16
19
|
empty `Tuple()` as one UInt8 (zero) per row (ClickHouse/ClickHouse#55061);
|
|
17
20
|
upstream reads nothing and desyncs the stream. Marked with
|
|
18
21
|
`LOCAL PATCH (ch_connect)`. Should be upstreamed.
|
|
@@ -47,8 +47,15 @@ int chc_async_submit(chc_async_client *c, const void *buf, size_t len,
|
|
|
47
47
|
void chc_async_pending_out(chc_async_client *c, const uint8_t **buf, size_t *len);
|
|
48
48
|
void chc_async_consume_out(chc_async_client *c, size_t n);
|
|
49
49
|
|
|
50
|
-
/*
|
|
51
|
-
|
|
50
|
+
/* Drive calls never block. CHC_WOULD_BLOCK requests more input or output
|
|
51
|
+
* draining
|
|
52
|
+
*
|
|
53
|
+
* Handshake rejection returns CHC_ERR_SERVER and leaves err->msg empty. When
|
|
54
|
+
* exc is not NULL, caller owns *exc and frees it with chc_exception_free.
|
|
55
|
+
* Other results leave *exc unchanged. Query exceptions still arrive as
|
|
56
|
+
* packets, but handshake rejection ends connection setup */
|
|
57
|
+
int chc_async_handshake(chc_async_client *c, chc_exception **exc,
|
|
58
|
+
chc_err *err);
|
|
52
59
|
int chc_async_send_query(chc_async_client *c, const char *sql, size_t sql_len,
|
|
53
60
|
const char *query_id, size_t query_id_len, chc_err *err);
|
|
54
61
|
int chc_async_send_data(chc_async_client *c, const chc_block_builder *bb,
|
|
@@ -122,16 +129,7 @@ chc_async_client_init(chc_async_client **out, const chc_client_opts *opts,
|
|
|
122
129
|
if (!c) return CHC_ERR_OOM;
|
|
123
130
|
|
|
124
131
|
c->cli.al = al;
|
|
125
|
-
c->cli
|
|
126
|
-
c->cli.client_version_minor = opts->client_version_minor;
|
|
127
|
-
c->cli.client_version_patch = opts->client_version_patch;
|
|
128
|
-
c->cli.client_revision = opts->client_revision ? opts->client_revision
|
|
129
|
-
: CHC_CLIENT_DEFAULT_REVISION;
|
|
130
|
-
c->cli.compression = opts->codec ? opts->compression : CHC_COMP_NONE;
|
|
131
|
-
c->cli.codec = opts->codec;
|
|
132
|
-
/* Seed server.revision so block/packet framing is well-defined before the
|
|
133
|
-
* handshake completes; recv after handshake uses min(client, server). */
|
|
134
|
-
c->cli.server.revision = c->cli.client_revision;
|
|
132
|
+
chc__client_setup(&c->cli, opts);
|
|
135
133
|
|
|
136
134
|
chc__mem_sink_init(&c->out, &c->out_io, al);
|
|
137
135
|
c->cli.io = &c->out_io;
|
|
@@ -196,7 +194,7 @@ chc_async_consume_out(chc_async_client *c, size_t n)
|
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
int
|
|
199
|
-
chc_async_handshake(chc_async_client *c, chc_err *err)
|
|
197
|
+
chc_async_handshake(chc_async_client *c, chc_exception **exc, chc_err *err)
|
|
200
198
|
{
|
|
201
199
|
int rc;
|
|
202
200
|
chc_client *cli = &c->cli;
|
|
@@ -209,7 +207,6 @@ chc_async_handshake(chc_async_client *c, chc_err *err)
|
|
|
209
207
|
.client_version_major = cli->client_version_major,
|
|
210
208
|
.client_version_minor = cli->client_version_minor,
|
|
211
209
|
.client_version_patch = cli->client_version_patch,
|
|
212
|
-
.client_revision = cli->client_revision,
|
|
213
210
|
.database = c->database,
|
|
214
211
|
.user = c->user,
|
|
215
212
|
.password = c->password,
|
|
@@ -222,27 +219,23 @@ chc_async_handshake(chc_async_client *c, chc_err *err)
|
|
|
222
219
|
|
|
223
220
|
case CHC__HS_RECV_HELLO:
|
|
224
221
|
chc__in_checkpoint(&cli->in);
|
|
225
|
-
rc = chc__client_recv_hello(cli, err);
|
|
222
|
+
rc = chc__client_recv_hello(cli, exc, err);
|
|
226
223
|
if (rc == CHC_WOULD_BLOCK) { chc__in_rewind(&cli->in); return rc; }
|
|
227
224
|
if (rc != CHC_OK) return rc; /* server exception / protocol */
|
|
228
225
|
chc_in_reset(&cli->in);
|
|
229
|
-
if (cli->server.revision > cli->client_revision)
|
|
230
|
-
cli->server.revision = cli->client_revision;
|
|
231
226
|
c->hs_phase = CHC__HS_POST_HELLO;
|
|
232
227
|
continue;
|
|
233
228
|
|
|
234
229
|
case CHC__HS_POST_HELLO:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (rc != CHC_OK) return rc;
|
|
238
|
-
}
|
|
230
|
+
rc = chc__write_string(cli->io, "", 0, err); /* quota_key */
|
|
231
|
+
if (rc != CHC_OK) return rc;
|
|
239
232
|
rc = chc_client_send_ping(cli, err);
|
|
240
233
|
if (rc != CHC_OK) return rc;
|
|
241
234
|
c->hs_phase = CHC__HS_RECV_PONG;
|
|
242
235
|
continue;
|
|
243
236
|
|
|
244
237
|
case CHC__HS_RECV_PONG:
|
|
245
|
-
rc = chc__recv_pong(cli, err); /* owns checkpoint/rewind */
|
|
238
|
+
rc = chc__recv_pong(cli, exc, err); /* owns checkpoint/rewind */
|
|
246
239
|
if (rc != CHC_OK) return rc;
|
|
247
240
|
chc_in_reset(&cli->in);
|
|
248
241
|
c->hs_phase = CHC__HS_DONE;
|