ch_connect 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +1 -1
- data/ext/ch_connect_native/ch_connect_native.c +125 -22
- 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: 5856233f30844a931e61ac5aca11af740c7a876a95ae19e497a3157bca9097b2
|
|
4
|
+
data.tar.gz: 6588fea1311f26c52e6b13885316741e47a98a284751456d9a5c3e022d036c04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a2650b889220c5b2e66803f0da23022357fd6c6997e7d09841b983768d099bb487b4e6ad51c8eadb18e255e966afa318d7652a176dd01a44819a43989bb1aef
|
|
7
|
+
data.tar.gz: 0310c5b3de97b1388d85eab045c1c21ff8ab5ab7354cc092683e1c383532c93c77ca6cdffd3bc86b0bdcd6d8ae943b26c3eb00616e996f732dd9f658ce892485
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## [0.4.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
- Raised the minimum supported ClickHouse version to 23.3; connections to older servers are rejected during the handshake
|
|
6
|
+
- Updated vendored clickhouse-c headers to `916cbf5`, adding sparse column decoding
|
|
7
|
+
- Improved decimal decoding performance while preserving exact values across all decimal widths
|
|
8
|
+
- Fixed decoding of geometry types, including Point, Ring, Polygon, MultiPolygon, LineString, and MultiLineString
|
|
9
|
+
- Preserved complete UTF-8 server error messages during connection establishment
|
|
10
|
+
|
|
1
11
|
## [0.3.1] - 2026-08-19
|
|
2
12
|
|
|
3
13
|
- Fixed LZ4 compression negotiation for ClickHouse users configured with `readonly = 1`
|
data/README.md
CHANGED
|
@@ -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;
|
|
@@ -136,6 +135,39 @@ load_i64le(const uint8_t *p)
|
|
|
136
135
|
return v;
|
|
137
136
|
}
|
|
138
137
|
|
|
138
|
+
/* Little-endian two's complement value of 4, 8, 16 or 32 bytes as int64, when it fits. */
|
|
139
|
+
static int
|
|
140
|
+
load_decimal_i64(const uint8_t *p, size_t nbytes, int64_t *out)
|
|
141
|
+
{
|
|
142
|
+
if (nbytes == 4) {
|
|
143
|
+
*out = load_i32le(p);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
int64_t v = load_i64le(p);
|
|
147
|
+
uint8_t sign = v < 0 ? 0xFF : 0x00;
|
|
148
|
+
for (size_t b = 8; b < nbytes; b++) {
|
|
149
|
+
if (p[b] != sign) return 0;
|
|
150
|
+
}
|
|
151
|
+
*out = v;
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* "<unscaled>e-<scale>" for a decimal value. BigDecimal parses it exactly,
|
|
156
|
+
* without the intermediate BigDecimals of dividing by 10**scale. Only values
|
|
157
|
+
* outside the int64 range go through a Ruby Integer. */
|
|
158
|
+
static VALUE
|
|
159
|
+
decimal_literal(const uint8_t *p, size_t nbytes, int scale)
|
|
160
|
+
{
|
|
161
|
+
int64_t v;
|
|
162
|
+
if (load_decimal_i64(p, nbytes, &v)) {
|
|
163
|
+
char buf[48];
|
|
164
|
+
int len = snprintf(buf, sizeof(buf), "%" PRId64 "e-%d", v, scale);
|
|
165
|
+
return rb_usascii_str_new(buf, len);
|
|
166
|
+
}
|
|
167
|
+
VALUE unscaled = rb_integer_unpack(p, nbytes, 1, 0, INTEGER_PACK_LITTLE_ENDIAN | INTEGER_PACK_2COMP);
|
|
168
|
+
return rb_str_catf(rb_big2str(unscaled, 10), "e-%d", scale);
|
|
169
|
+
}
|
|
170
|
+
|
|
139
171
|
static inline float
|
|
140
172
|
load_f32le(const uint8_t *p)
|
|
141
173
|
{
|
|
@@ -334,23 +366,9 @@ decode_fixed(const chc_column *col, const chc_type *t, long n_rows, native_state
|
|
|
334
366
|
case CHC_DECIMAL128:
|
|
335
367
|
case CHC_DECIMAL256: {
|
|
336
368
|
int scale = chc_type_decimal_scale(t);
|
|
337
|
-
|
|
369
|
+
size_t nbytes = kind == CHC_DECIMAL32 ? 4 : kind == CHC_DECIMAL64 ? 8 : kind == CHC_DECIMAL128 ? 16 : 32;
|
|
338
370
|
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));
|
|
371
|
+
rb_ary_push(ary, rb_funcall(rb_mKernel, id_BigDecimal, 1, decimal_literal(data + i * nbytes, nbytes, scale)));
|
|
354
372
|
}
|
|
355
373
|
break;
|
|
356
374
|
}
|
|
@@ -377,9 +395,85 @@ decode_string_column(const chc_column *col, long n_rows)
|
|
|
377
395
|
return ary;
|
|
378
396
|
}
|
|
379
397
|
|
|
398
|
+
static void
|
|
399
|
+
raise_invalid_geometry(native_state *state, const char *detail)
|
|
400
|
+
{
|
|
401
|
+
*state = NATIVE_BROKEN;
|
|
402
|
+
rb_raise(eConnectionError, "Invalid native geometry column: %s", detail);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* clickhouse-c exposes geometry aliases as their wire-level Array/Tuple
|
|
406
|
+
* column tree, while the logical geometry type has no synthetic children. */
|
|
407
|
+
static VALUE
|
|
408
|
+
decode_geometry(const chc_column *col, long n_rows, int depth, native_state *state)
|
|
409
|
+
{
|
|
410
|
+
if (n_rows == 0) return rb_ary_new();
|
|
411
|
+
|
|
412
|
+
if (depth == 0) {
|
|
413
|
+
if (chc_column_layout(col) != CHC_COL_TUPLE ||
|
|
414
|
+
chc_column_tuple_arity(col) != 2) {
|
|
415
|
+
raise_invalid_geometry(state, "Point must be a two-coordinate tuple");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const chc_column *x_col = chc_column_tuple_child(col, 0);
|
|
419
|
+
const chc_column *y_col = chc_column_tuple_child(col, 1);
|
|
420
|
+
size_t x_size = 0;
|
|
421
|
+
size_t y_size = 0;
|
|
422
|
+
const uint8_t *x = (const uint8_t *)chc_column_fixed_data(x_col, &x_size);
|
|
423
|
+
const uint8_t *y = (const uint8_t *)chc_column_fixed_data(y_col, &y_size);
|
|
424
|
+
if (!x || !y || x_size != 8 || y_size != 8) {
|
|
425
|
+
raise_invalid_geometry(state, "Point coordinates must be Float64 columns");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
VALUE points = rb_ary_new_capa(n_rows);
|
|
429
|
+
for (long i = 0; i < n_rows; i++) {
|
|
430
|
+
VALUE point = rb_ary_new_capa(2);
|
|
431
|
+
rb_ary_push(point, DBL2NUM(load_f64le(x + i * 8)));
|
|
432
|
+
rb_ary_push(point, DBL2NUM(load_f64le(y + i * 8)));
|
|
433
|
+
rb_ary_push(points, point);
|
|
434
|
+
}
|
|
435
|
+
return points;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (chc_column_layout(col) != CHC_COL_ARRAY) {
|
|
439
|
+
raise_invalid_geometry(state, "expected an Array layer");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const uint64_t *offsets = chc_column_array_offsets(col);
|
|
443
|
+
const chc_column *values_col = chc_column_array_values(col);
|
|
444
|
+
long total = (long)chc_column_n_rows(values_col);
|
|
445
|
+
VALUE elements = decode_geometry(values_col, total, depth - 1, state);
|
|
446
|
+
VALUE ary = rb_ary_new_capa(n_rows);
|
|
447
|
+
uint64_t start = 0;
|
|
448
|
+
for (long i = 0; i < n_rows; i++) {
|
|
449
|
+
uint64_t end = offsets[i];
|
|
450
|
+
if (end < start || end > (uint64_t)RARRAY_LEN(elements)) {
|
|
451
|
+
raise_invalid_geometry(state, "Array offset is out of bounds");
|
|
452
|
+
}
|
|
453
|
+
rb_ary_push(ary, rb_ary_subseq(elements, (long)start, (long)(end - start)));
|
|
454
|
+
start = end;
|
|
455
|
+
}
|
|
456
|
+
return ary;
|
|
457
|
+
}
|
|
458
|
+
|
|
380
459
|
static VALUE
|
|
381
460
|
decode_column(const chc_column *col, const chc_type *t, long n_rows, native_state *state)
|
|
382
461
|
{
|
|
462
|
+
switch (chc_type_kind(t)) {
|
|
463
|
+
case CHC_POINT:
|
|
464
|
+
return decode_geometry(col, n_rows, 0, state);
|
|
465
|
+
case CHC_RING:
|
|
466
|
+
case CHC_LINE_STRING:
|
|
467
|
+
return decode_geometry(col, n_rows, 1, state);
|
|
468
|
+
case CHC_POLYGON:
|
|
469
|
+
case CHC_MULTI_LINE_STRING:
|
|
470
|
+
return decode_geometry(col, n_rows, 2, state);
|
|
471
|
+
case CHC_MULTI_POLYGON:
|
|
472
|
+
return decode_geometry(col, n_rows, 3, state);
|
|
473
|
+
default:
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
383
477
|
if (n_rows == 0) {
|
|
384
478
|
chc_kind kind = chc_type_kind(t);
|
|
385
479
|
if (kind == CHC_STRING) return rb_ary_new();
|
|
@@ -638,7 +732,8 @@ native_client_handshake_step(VALUE self)
|
|
|
638
732
|
|
|
639
733
|
nc->state = NATIVE_ACTIVE;
|
|
640
734
|
chc_err err = {0};
|
|
641
|
-
|
|
735
|
+
chc_exception *exception = NULL;
|
|
736
|
+
int rc = chc_async_handshake(nc->ac, &exception, &err);
|
|
642
737
|
if (rc == CHC_OK) {
|
|
643
738
|
nc->state = NATIVE_READY;
|
|
644
739
|
return sym_done;
|
|
@@ -646,6 +741,16 @@ native_client_handshake_step(VALUE self)
|
|
|
646
741
|
if (rc == CHC_WOULD_BLOCK) return sym_want_read;
|
|
647
742
|
|
|
648
743
|
nc->state = NATIVE_BROKEN;
|
|
744
|
+
if (exception) {
|
|
745
|
+
/* Anchor the server exception before allocating Ruby objects so close
|
|
746
|
+
* or GC can still release it if message construction raises. */
|
|
747
|
+
nc->pending_pkt.kind = CHC_PKT_EXCEPTION;
|
|
748
|
+
nc->pending_pkt.exception = exception;
|
|
749
|
+
VALUE msg = rb_utf8_str_new(exception->display_text,
|
|
750
|
+
(long)exception->display_text_len);
|
|
751
|
+
clear_pending(nc);
|
|
752
|
+
rb_exc_raise(rb_exc_new_str(eConnectionError, msg));
|
|
753
|
+
}
|
|
649
754
|
rb_raise(eConnectionError, "%s", err.msg);
|
|
650
755
|
}
|
|
651
756
|
|
|
@@ -880,7 +985,7 @@ native_client_recv_step(VALUE self)
|
|
|
880
985
|
nc->state = NATIVE_READY;
|
|
881
986
|
return sym_done;
|
|
882
987
|
}
|
|
883
|
-
/* PONG / TOTALS / EXTREMES / LOG / PROFILE_EVENTS — skip. */
|
|
988
|
+
/* PONG / TOTALS / EXTREMES / LOG / PROFILE_EVENTS / TIMEZONE_UPDATE — skip. */
|
|
884
989
|
}
|
|
885
990
|
}
|
|
886
991
|
|
|
@@ -969,8 +1074,6 @@ Init_ch_connect_native(void)
|
|
|
969
1074
|
|
|
970
1075
|
id_jd = rb_intern("jd");
|
|
971
1076
|
id_new = rb_intern("new");
|
|
972
|
-
id_pow = rb_intern("**");
|
|
973
|
-
id_div = rb_intern("/");
|
|
974
1077
|
id_BigDecimal = rb_intern("BigDecimal");
|
|
975
1078
|
|
|
976
1079
|
sym_read_rows = ID2SYM(rb_intern("read_rows"));
|
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;
|