clickhouse-native 0.0.1 → 0.1.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/ext/clickhouse_native/client.cpp +235 -15
- data/ext/clickhouse_native/extconf.rb +53 -14
- data/lib/clickhouse_native/client.rb +37 -21
- data/lib/clickhouse_native/errors.rb +2 -0
- data/lib/clickhouse_native/logging.rb +64 -0
- data/lib/clickhouse_native/pool.rb +6 -4
- data/lib/clickhouse_native/version.rb +3 -1
- data/lib/clickhouse_native.rb +5 -0
- metadata +11 -63
- data/lib/clickhouse_native/clickhouse_native.bundle +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51aa32b5ec77367fcc08b8df05c2449bb8e99b7f1cc3b7cc331d83c408cea32c
|
|
4
|
+
data.tar.gz: 3b28a9d0cde5d7c827f212bb5ecde1d0dee21f5a8e4cc2faaa20de06534f59d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 825b57cd31290f4717a5e38d7b0d08c7949e455c23246913f1885db868a63b0b44a600f9527dd5f7175f889d1e9327f7b75af4ada2075aa5d2b7284f0b88184c
|
|
7
|
+
data.tar.gz: 6c178edde2ba5ffbd1bee2f439e8d5c2d3dd3932c80442b9da34090cfc3286388a2cf3b834ff43a476aa7c68d1aaedc91409b48287a3e940383d8f8d98a75222
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <clickhouse/client.h>
|
|
5
5
|
#include <clickhouse/columns/array.h>
|
|
6
6
|
#include <clickhouse/columns/date.h>
|
|
7
|
+
#include <clickhouse/columns/decimal.h>
|
|
7
8
|
#include <clickhouse/columns/enum.h>
|
|
8
9
|
#include <clickhouse/columns/factory.h>
|
|
9
10
|
#include <clickhouse/columns/lowcardinality.h>
|
|
@@ -30,12 +31,21 @@ static VALUE rb_cClient;
|
|
|
30
31
|
static VALUE err_base, err_connection, err_timeout, err_protocol,
|
|
31
32
|
err_server, err_encoder, err_decoder, err_unsupported;
|
|
32
33
|
|
|
33
|
-
// Internal
|
|
34
|
-
// raise_mapped_ex -> err_encoder
|
|
34
|
+
// Internal exceptions used to tag encoder / decoder failures and drive
|
|
35
|
+
// them through raise_mapped_ex -> err_encoder / err_unsupported /
|
|
36
|
+
// err_decoder without rb_raising from inside a try block. Critically,
|
|
37
|
+
// throwing (instead of rb_raise) lets the outer catch run
|
|
38
|
+
// ResetConnection() so the pooled client doesn't stay mid-packet.
|
|
35
39
|
namespace chn {
|
|
36
40
|
class EncoderFailure : public clickhouse::Error {
|
|
37
41
|
using clickhouse::Error::Error;
|
|
38
42
|
};
|
|
43
|
+
class UnsupportedType : public clickhouse::Error {
|
|
44
|
+
using clickhouse::Error::Error;
|
|
45
|
+
};
|
|
46
|
+
class DecoderFailure : public clickhouse::Error {
|
|
47
|
+
using clickhouse::Error::Error;
|
|
48
|
+
};
|
|
39
49
|
} // namespace chn
|
|
40
50
|
|
|
41
51
|
// ------------------------------------------------------------------
|
|
@@ -56,6 +66,12 @@ static void raise_mapped_ex(const std::exception& e) {
|
|
|
56
66
|
if (dynamic_cast<const chn::EncoderFailure*>(&e)) {
|
|
57
67
|
rb_raise(err_encoder, "%s", e.what());
|
|
58
68
|
}
|
|
69
|
+
if (dynamic_cast<const chn::UnsupportedType*>(&e)) {
|
|
70
|
+
rb_raise(err_unsupported, "%s", e.what());
|
|
71
|
+
}
|
|
72
|
+
if (dynamic_cast<const chn::DecoderFailure*>(&e)) {
|
|
73
|
+
rb_raise(err_decoder, "%s", e.what());
|
|
74
|
+
}
|
|
59
75
|
if (dynamic_cast<const ProtocolError*>(&e)) {
|
|
60
76
|
rb_raise(err_protocol, "%s", e.what());
|
|
61
77
|
}
|
|
@@ -99,6 +115,55 @@ static VALUE value_at(const ColumnRef& col, size_t idx) {
|
|
|
99
115
|
case Type::Float32: return DBL2NUM(col->As<ColumnFloat32>()->At(idx));
|
|
100
116
|
case Type::Float64: return DBL2NUM(col->As<ColumnFloat64>()->At(idx));
|
|
101
117
|
|
|
118
|
+
case Type::Decimal:
|
|
119
|
+
case Type::Decimal32:
|
|
120
|
+
case Type::Decimal64:
|
|
121
|
+
case Type::Decimal128: {
|
|
122
|
+
// ClickHouse stores decimals as scaled integers. clickhouse-cpp
|
|
123
|
+
// returns them as Int128. To preserve precision, we format the
|
|
124
|
+
// unscaled int into a string at the column's scale and construct
|
|
125
|
+
// a BigDecimal from it.
|
|
126
|
+
auto dec = col->As<ColumnDecimal>();
|
|
127
|
+
auto unscaled = dec->At(idx); // Int128 (absl::int128)
|
|
128
|
+
size_t scale = type->As<DecimalType>()->GetScale();
|
|
129
|
+
|
|
130
|
+
bool neg = unscaled < 0;
|
|
131
|
+
absl::uint128 mag = neg
|
|
132
|
+
? absl::uint128(-static_cast<absl::int128>(unscaled))
|
|
133
|
+
: absl::uint128(static_cast<absl::int128>(unscaled));
|
|
134
|
+
|
|
135
|
+
// Render magnitude in base 10, then place the decimal point.
|
|
136
|
+
char buf[48];
|
|
137
|
+
int len = 0;
|
|
138
|
+
if (mag == absl::uint128(0)) {
|
|
139
|
+
buf[len++] = '0';
|
|
140
|
+
} else {
|
|
141
|
+
while (mag > absl::uint128(0)) {
|
|
142
|
+
absl::uint128 q = mag / absl::uint128(10);
|
|
143
|
+
absl::uint128 r = mag - q * absl::uint128(10);
|
|
144
|
+
buf[len++] = '0' + static_cast<int>(absl::Uint128Low64(r));
|
|
145
|
+
mag = q;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Pad with leading zeros so we have at least scale+1 digits.
|
|
149
|
+
while (static_cast<size_t>(len) <= scale) buf[len++] = '0';
|
|
150
|
+
|
|
151
|
+
std::string out;
|
|
152
|
+
out.reserve(len + 3);
|
|
153
|
+
if (neg) out.push_back('-');
|
|
154
|
+
for (int i = len - 1; i >= 0; i--) {
|
|
155
|
+
if (scale > 0 && static_cast<size_t>(i) == scale - 1) {
|
|
156
|
+
// Insert decimal point before the last `scale` digits.
|
|
157
|
+
out.push_back('.');
|
|
158
|
+
}
|
|
159
|
+
out.push_back(buf[i]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
VALUE rb_cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal"));
|
|
163
|
+
return rb_funcall(rb_cBigDecimal, rb_intern("BigDecimal"), 1,
|
|
164
|
+
rb_utf8_str_new(out.data(), out.size()));
|
|
165
|
+
}
|
|
166
|
+
|
|
102
167
|
case Type::String: {
|
|
103
168
|
auto sv = col->As<ColumnString>()->At(idx);
|
|
104
169
|
return rb_utf8_str_new(sv.data(), sv.size());
|
|
@@ -149,7 +214,11 @@ static VALUE value_at(const ColumnRef& col, size_t idx) {
|
|
|
149
214
|
|
|
150
215
|
case Type::LowCardinality: {
|
|
151
216
|
auto lc = col->As<ColumnLowCardinality>();
|
|
152
|
-
|
|
217
|
+
ItemView item = lc->GetItem(idx);
|
|
218
|
+
// For LowCardinality(Nullable(T)), clickhouse-cpp's GetItem returns
|
|
219
|
+
// a default-constructed ItemView (type == Void) for null rows.
|
|
220
|
+
if (item.type == Type::Void) return Qnil;
|
|
221
|
+
auto sv = item.AsBinaryData();
|
|
153
222
|
return rb_utf8_str_new(sv.data(), sv.size());
|
|
154
223
|
}
|
|
155
224
|
|
|
@@ -158,7 +227,7 @@ static VALUE value_at(const ColumnRef& col, size_t idx) {
|
|
|
158
227
|
auto tuples = map_col->GetAsColumn(idx);
|
|
159
228
|
auto tuple = tuples->As<ColumnTuple>();
|
|
160
229
|
if (!tuple || tuple->TupleSize() != 2) {
|
|
161
|
-
|
|
230
|
+
throw chn::DecoderFailure("clickhouse-native: malformed Map column");
|
|
162
231
|
}
|
|
163
232
|
auto keys = (*tuple)[0];
|
|
164
233
|
auto vals = (*tuple)[1];
|
|
@@ -188,9 +257,10 @@ static VALUE value_at(const ColumnRef& col, size_t idx) {
|
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
default:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
260
|
+
throw chn::UnsupportedType(
|
|
261
|
+
std::string("clickhouse-native: unsupported column type ")
|
|
262
|
+
+ type->GetName()
|
|
263
|
+
+ " (code=" + std::to_string(static_cast<int>(type->GetCode())) + ")");
|
|
194
264
|
}
|
|
195
265
|
return Qnil;
|
|
196
266
|
}
|
|
@@ -222,6 +292,26 @@ static void append_default(const ColumnRef& col) {
|
|
|
222
292
|
case Type::Date32: col->As<ColumnDate32>()->Append(0); return;
|
|
223
293
|
case Type::DateTime: col->As<ColumnDateTime>()->Append(0); return;
|
|
224
294
|
case Type::DateTime64: col->As<ColumnDateTime64>()->Append(0); return;
|
|
295
|
+
case Type::Decimal:
|
|
296
|
+
case Type::Decimal32:
|
|
297
|
+
case Type::Decimal64:
|
|
298
|
+
case Type::Decimal128: col->As<ColumnDecimal>()->Append(std::string("0")); return;
|
|
299
|
+
case Type::LowCardinality: {
|
|
300
|
+
auto nested_type = type->As<LowCardinalityType>()->GetNestedType();
|
|
301
|
+
bool nullable = nested_type->GetCode() == Type::Nullable;
|
|
302
|
+
if (!nullable) {
|
|
303
|
+
auto lct = col->AsStrict<ColumnLowCardinalityT<ColumnString>>();
|
|
304
|
+
lct->Append(std::string_view());
|
|
305
|
+
} else {
|
|
306
|
+
auto tmp_inner = std::make_shared<ColumnString>();
|
|
307
|
+
auto tmp_nulls = std::make_shared<ColumnUInt8>();
|
|
308
|
+
tmp_inner->Append(std::string_view());
|
|
309
|
+
tmp_nulls->Append(1);
|
|
310
|
+
auto tmp = std::make_shared<ColumnNullable>(tmp_inner, tmp_nulls);
|
|
311
|
+
col->As<ColumnLowCardinality>()->Append(tmp);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
225
315
|
case Type::Array: {
|
|
226
316
|
auto inner_type = type->As<ArrayType>()->GetItemType();
|
|
227
317
|
auto inner_col = CreateColumnByType(inner_type->GetName());
|
|
@@ -234,6 +324,51 @@ static void append_default(const ColumnRef& col) {
|
|
|
234
324
|
}
|
|
235
325
|
}
|
|
236
326
|
|
|
327
|
+
// Detect a trailing timezone indicator: "Z", "+HHMM", "+HH:MM", or the
|
|
328
|
+
// same with '-' (anywhere after the date portion, so the '-' inside
|
|
329
|
+
// "YYYY-MM-DD" doesn't match). Used to decide whether a naked timestamp
|
|
330
|
+
// string should be forced to UTC.
|
|
331
|
+
static bool has_trailing_tz(const char* s, size_t len) {
|
|
332
|
+
if (len == 0) return false;
|
|
333
|
+
if (s[len - 1] == 'Z' || s[len - 1] == 'z') return true;
|
|
334
|
+
// walk backwards for a '+' or '-' within the last 6 chars (HH:MM / HHMM)
|
|
335
|
+
size_t limit = len > 6 ? len - 6 : 0;
|
|
336
|
+
for (size_t i = len; i-- > limit; ) {
|
|
337
|
+
if (i < 11) break; // position must be past "YYYY-MM-DD "
|
|
338
|
+
if (s[i] == '+' || s[i] == '-') return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Accepts a Time, a String (parsed via Time.parse), or a numeric (epoch
|
|
344
|
+
// seconds). Mirrors how the HTTP gem's JSONEachRow coerced date cells.
|
|
345
|
+
//
|
|
346
|
+
// Naked timestamp strings (no trailing TZ indicator) are parsed as UTC —
|
|
347
|
+
// CH's DateTime64(N, 'UTC') convention. Strings with explicit zones are
|
|
348
|
+
// respected.
|
|
349
|
+
static VALUE coerce_to_time(VALUE value) {
|
|
350
|
+
if (rb_obj_is_kind_of(value, rb_cTime)) return value;
|
|
351
|
+
if (RB_TYPE_P(value, T_STRING)) {
|
|
352
|
+
const char* p = RSTRING_PTR(value);
|
|
353
|
+
long n = RSTRING_LEN(value);
|
|
354
|
+
VALUE to_parse = value;
|
|
355
|
+
if (!has_trailing_tz(p, n)) {
|
|
356
|
+
std::string with_utc(p, n);
|
|
357
|
+
with_utc.append(" UTC");
|
|
358
|
+
to_parse = rb_utf8_str_new(with_utc.data(), with_utc.size());
|
|
359
|
+
}
|
|
360
|
+
return rb_funcall(rb_cTime, rb_intern("parse"), 1, to_parse);
|
|
361
|
+
}
|
|
362
|
+
if (RB_INTEGER_TYPE_P(value) || RB_FLOAT_TYPE_P(value)) {
|
|
363
|
+
return rb_funcall(rb_cTime, rb_intern("at"), 1, value);
|
|
364
|
+
}
|
|
365
|
+
// Date / DateTime / other responds to to_time
|
|
366
|
+
if (rb_respond_to(value, rb_intern("to_time"))) {
|
|
367
|
+
return rb_funcall(value, rb_intern("to_time"), 0);
|
|
368
|
+
}
|
|
369
|
+
throw chn::EncoderFailure("cannot coerce value to Time");
|
|
370
|
+
}
|
|
371
|
+
|
|
237
372
|
static int64_t time_to_datetime64_ticks(VALUE value, size_t prec) {
|
|
238
373
|
VALUE to_i = rb_funcall(value, rb_intern("to_i"), 0);
|
|
239
374
|
VALUE nsec = rb_funcall(value, rb_intern("nsec"), 0);
|
|
@@ -246,6 +381,29 @@ static int64_t time_to_datetime64_ticks(VALUE value, size_t prec) {
|
|
|
246
381
|
|
|
247
382
|
static void append_value(const ColumnRef& col, VALUE value) {
|
|
248
383
|
auto type = col->Type();
|
|
384
|
+
// Graceful nil handling for non-Nullable columns — mirrors how the HTTP
|
|
385
|
+
// gem's JSONEachRow insert silently coerced nil to the column default.
|
|
386
|
+
// Structural types (Nullable/Array/Map/Tuple/LowCardinality) have their
|
|
387
|
+
// own nil semantics handled below.
|
|
388
|
+
if (NIL_P(value)) {
|
|
389
|
+
auto code = type->GetCode();
|
|
390
|
+
// Structural types handle nil via their own semantics below.
|
|
391
|
+
if (code != Type::Nullable && code != Type::Array &&
|
|
392
|
+
code != Type::Map && code != Type::Tuple) {
|
|
393
|
+
append_default(col);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Bool columns come over the wire as UInt8. Ruby true/false are the
|
|
398
|
+
// natural inputs for Bool, so coerce here before the numeric cases run.
|
|
399
|
+
if (value == Qtrue) value = INT2FIX(1);
|
|
400
|
+
else if (value == Qfalse) value = INT2FIX(0);
|
|
401
|
+
|
|
402
|
+
// Symbol -> String: common for LowCardinality/String columns where
|
|
403
|
+
// callers naturally use :pt, :eur, etc. Matches how JSON serialization
|
|
404
|
+
// in the HTTP path rendered them.
|
|
405
|
+
if (SYMBOL_P(value)) value = rb_funcall(value, rb_intern("to_s"), 0);
|
|
406
|
+
|
|
249
407
|
switch (type->GetCode()) {
|
|
250
408
|
case Type::Int8: col->As<ColumnInt8>()->Append(NUM2INT(value)); return;
|
|
251
409
|
case Type::Int16: col->As<ColumnInt16>()->Append(NUM2INT(value)); return;
|
|
@@ -272,23 +430,24 @@ static void append_value(const ColumnRef& col, VALUE value) {
|
|
|
272
430
|
}
|
|
273
431
|
|
|
274
432
|
case Type::Date: {
|
|
275
|
-
VALUE
|
|
276
|
-
col->As<ColumnDate>()->Append(static_cast<std::time_t>(NUM2LL(to_i)));
|
|
433
|
+
VALUE t = coerce_to_time(value);
|
|
434
|
+
col->As<ColumnDate>()->Append(static_cast<std::time_t>(NUM2LL(rb_funcall(t, rb_intern("to_i"), 0))));
|
|
277
435
|
return;
|
|
278
436
|
}
|
|
279
437
|
case Type::Date32: {
|
|
280
|
-
VALUE
|
|
281
|
-
col->As<ColumnDate32>()->Append(static_cast<std::time_t>(NUM2LL(to_i)));
|
|
438
|
+
VALUE t = coerce_to_time(value);
|
|
439
|
+
col->As<ColumnDate32>()->Append(static_cast<std::time_t>(NUM2LL(rb_funcall(t, rb_intern("to_i"), 0))));
|
|
282
440
|
return;
|
|
283
441
|
}
|
|
284
442
|
case Type::DateTime: {
|
|
285
|
-
VALUE
|
|
286
|
-
col->As<ColumnDateTime>()->Append(static_cast<std::time_t>(NUM2LL(to_i)));
|
|
443
|
+
VALUE t = coerce_to_time(value);
|
|
444
|
+
col->As<ColumnDateTime>()->Append(static_cast<std::time_t>(NUM2LL(rb_funcall(t, rb_intern("to_i"), 0))));
|
|
287
445
|
return;
|
|
288
446
|
}
|
|
289
447
|
case Type::DateTime64: {
|
|
290
448
|
size_t prec = type->As<DateTime64Type>()->GetPrecision();
|
|
291
|
-
|
|
449
|
+
VALUE t = coerce_to_time(value);
|
|
450
|
+
col->As<ColumnDateTime64>()->Append(time_to_datetime64_ticks(t, prec));
|
|
292
451
|
return;
|
|
293
452
|
}
|
|
294
453
|
|
|
@@ -321,6 +480,63 @@ static void append_value(const ColumnRef& col, VALUE value) {
|
|
|
321
480
|
return;
|
|
322
481
|
}
|
|
323
482
|
|
|
483
|
+
case Type::Decimal:
|
|
484
|
+
case Type::Decimal32:
|
|
485
|
+
case Type::Decimal64:
|
|
486
|
+
case Type::Decimal128: {
|
|
487
|
+
// clickhouse-cpp's Decimal::Append(string) only scales up when a
|
|
488
|
+
// decimal point is present (see columns/decimal.cpp). "1" at
|
|
489
|
+
// scale 8 is parsed as unscaled 1 => 1e-8, not 1. Normalise
|
|
490
|
+
// through BigDecimal#to_s("F") so Integer / Float / BigDecimal
|
|
491
|
+
// all land as a fixed-point string.
|
|
492
|
+
VALUE bd = rb_funcall(rb_cObject, rb_intern("BigDecimal"), 1,
|
|
493
|
+
rb_funcall(value, rb_intern("to_s"), 0));
|
|
494
|
+
VALUE str = rb_funcall(bd, rb_intern("to_s"), 1,
|
|
495
|
+
rb_utf8_str_new_cstr("F"));
|
|
496
|
+
col->As<ColumnDecimal>()->Append(
|
|
497
|
+
std::string(RSTRING_PTR(str), RSTRING_LEN(str)));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
case Type::LowCardinality: {
|
|
502
|
+
// Only LowCardinality(String) and LowCardinality(Nullable(String))
|
|
503
|
+
// are supported for insert in v1 — this covers the profile-service
|
|
504
|
+
// use case. Numeric LC dictionaries are rare and can wait.
|
|
505
|
+
auto nested_type = type->As<LowCardinalityType>()->GetNestedType();
|
|
506
|
+
bool nullable = nested_type->GetCode() == Type::Nullable;
|
|
507
|
+
auto inner_type = nullable
|
|
508
|
+
? nested_type->As<NullableType>()->GetNestedType()
|
|
509
|
+
: nested_type;
|
|
510
|
+
auto inner_code = inner_type->GetCode();
|
|
511
|
+
if (inner_code != Type::String && inner_code != Type::FixedString) {
|
|
512
|
+
throw chn::EncoderFailure(
|
|
513
|
+
"LowCardinality(" + nested_type->GetName() + ") insert not supported");
|
|
514
|
+
}
|
|
515
|
+
if (!nullable) {
|
|
516
|
+
auto lct = col->AsStrict<ColumnLowCardinalityT<ColumnString>>();
|
|
517
|
+
StringValue(value);
|
|
518
|
+
lct->Append(std::string_view(RSTRING_PTR(value), RSTRING_LEN(value)));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// LowCardinality(Nullable(T)): the factory returns base
|
|
522
|
+
// ColumnLowCardinality wrapping ColumnNullable, not the templated
|
|
523
|
+
// T form. Build a one-row ColumnNullable<String> and Append it —
|
|
524
|
+
// the base class handles merging into the LC dictionary.
|
|
525
|
+
auto tmp_inner = std::make_shared<ColumnString>();
|
|
526
|
+
auto tmp_nulls = std::make_shared<ColumnUInt8>();
|
|
527
|
+
if (NIL_P(value)) {
|
|
528
|
+
tmp_inner->Append(std::string_view());
|
|
529
|
+
tmp_nulls->Append(1);
|
|
530
|
+
} else {
|
|
531
|
+
StringValue(value);
|
|
532
|
+
tmp_inner->Append(std::string_view(RSTRING_PTR(value), RSTRING_LEN(value)));
|
|
533
|
+
tmp_nulls->Append(0);
|
|
534
|
+
}
|
|
535
|
+
auto tmp = std::make_shared<ColumnNullable>(tmp_inner, tmp_nulls);
|
|
536
|
+
col->As<ColumnLowCardinality>()->Append(tmp);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
324
540
|
default:
|
|
325
541
|
throw chn::EncoderFailure(
|
|
326
542
|
"cannot insert into column of type " + type->GetName());
|
|
@@ -415,6 +631,9 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
|
|
|
415
631
|
rb_ivar_set(self, rb_intern("@host"), rb_utf8_str_new(host.data(), host.size()));
|
|
416
632
|
rb_ivar_set(self, rb_intern("@port"), UINT2NUM(port));
|
|
417
633
|
rb_ivar_set(self, rb_intern("@database"), rb_utf8_str_new(database.data(), database.size()));
|
|
634
|
+
|
|
635
|
+
VALUE logger = rb_hash_lookup2(kwargs, ID2SYM(rb_intern("logger")), Qnil);
|
|
636
|
+
rb_ivar_set(self, rb_intern("@logger"), logger);
|
|
418
637
|
return self;
|
|
419
638
|
}
|
|
420
639
|
|
|
@@ -466,7 +685,8 @@ static VALUE ch_client_execute(VALUE self, VALUE rb_sql) {
|
|
|
466
685
|
}
|
|
467
686
|
|
|
468
687
|
// ------------------------------------------------------------------
|
|
469
|
-
// query() —
|
|
688
|
+
// query() — buffers all rows into an array; GVL is held for the duration.
|
|
689
|
+
// See query_each below for a streaming, GVL-releasing variant.
|
|
470
690
|
// ------------------------------------------------------------------
|
|
471
691
|
|
|
472
692
|
static VALUE ch_client_query(VALUE self, VALUE rb_sql) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "mkmf"
|
|
2
4
|
require "fileutils"
|
|
3
5
|
require "etc"
|
|
@@ -5,7 +7,11 @@ require "shellwords"
|
|
|
5
7
|
|
|
6
8
|
EXT_DIR = __dir__
|
|
7
9
|
VENDOR = File.expand_path("vendor/clickhouse-cpp", EXT_DIR)
|
|
8
|
-
|
|
10
|
+
# Scope the cmake build dir by target arch so host and cross-compile
|
|
11
|
+
# builds don't share a CMakeCache.txt (which burns in absolute paths
|
|
12
|
+
# like the host cmake binary).
|
|
13
|
+
ARCH = RbConfig::CONFIG["arch"] || "unknown"
|
|
14
|
+
BUILD_DIR = File.expand_path("../../tmp/cpp-build-#{ARCH}", EXT_DIR)
|
|
9
15
|
|
|
10
16
|
def fatal(msg)
|
|
11
17
|
warn
|
|
@@ -46,6 +52,7 @@ unless find_executable("cmake")
|
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
cxx = ENV["CXX"] || RbConfig::CONFIG["CXX"] || "c++"
|
|
55
|
+
cc = ENV["CC"] || RbConfig::CONFIG["CC"] || "cc"
|
|
49
56
|
unless find_executable(cxx.split.first)
|
|
50
57
|
fatal <<~MSG
|
|
51
58
|
A C++17-capable compiler is required.
|
|
@@ -59,23 +66,49 @@ end
|
|
|
59
66
|
|
|
60
67
|
FileUtils.mkdir_p(BUILD_DIR)
|
|
61
68
|
|
|
69
|
+
darwin_cross = ARCH.include?("darwin") && Dir.exist?("/opt/osxcross")
|
|
70
|
+
|
|
71
|
+
build_env = {}
|
|
72
|
+
configure_args = [
|
|
73
|
+
"cmake",
|
|
74
|
+
"-S", VENDOR,
|
|
75
|
+
"-B", BUILD_DIR,
|
|
76
|
+
"-DCMAKE_C_COMPILER=#{cc.split.first}",
|
|
77
|
+
"-DCMAKE_CXX_COMPILER=#{cxx.split.first}",
|
|
78
|
+
"-DBUILD_SHARED_LIBS=OFF",
|
|
79
|
+
"-DBUILD_BENCHMARK=OFF",
|
|
80
|
+
"-DBUILD_TESTS=OFF",
|
|
81
|
+
"-DWITH_OPENSSL=OFF",
|
|
82
|
+
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON",
|
|
83
|
+
"-DCMAKE_BUILD_TYPE=Release"
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if darwin_cross
|
|
87
|
+
# osxcross defaults MACOSX_DEPLOYMENT_TARGET to 10.13, but
|
|
88
|
+
# clickhouse-cpp calls std::optional::value() which libc++ marks
|
|
89
|
+
# unavailable below 10.14. Force the deployment target and disable
|
|
90
|
+
# libc++'s availability annotations.
|
|
91
|
+
build_env["MACOSX_DEPLOYMENT_TARGET"] = "10.15"
|
|
92
|
+
darwin_cxxflags = "-mmacosx-version-min=10.15 -D_LIBCPP_DISABLE_AVAILABILITY"
|
|
93
|
+
configure_args << "-DCMAKE_C_FLAGS=#{darwin_cxxflags}"
|
|
94
|
+
configure_args << "-DCMAKE_CXX_FLAGS=#{darwin_cxxflags}"
|
|
95
|
+
# Mach-O static archives need the osxcross ar/ranlib; the host's
|
|
96
|
+
# GNU ar leaves them without a table of contents that Apple's ld
|
|
97
|
+
# can read. cmake resolves relative FILEPATH to CWD, so use the
|
|
98
|
+
# absolute osxcross bindir.
|
|
99
|
+
prefix = cxx.split.first.sub(/-clang\+\+.*\z/, "")
|
|
100
|
+
osxcross_bin = "/opt/osxcross/target/bin"
|
|
101
|
+
configure_args << "-DCMAKE_AR=#{osxcross_bin}/#{prefix}-ar"
|
|
102
|
+
configure_args << "-DCMAKE_RANLIB=#{osxcross_bin}/#{prefix}-ranlib"
|
|
103
|
+
end
|
|
104
|
+
|
|
62
105
|
unless File.exist?(File.join(BUILD_DIR, "CMakeCache.txt"))
|
|
63
|
-
configure_args
|
|
64
|
-
"cmake"
|
|
65
|
-
"-S", VENDOR,
|
|
66
|
-
"-B", BUILD_DIR,
|
|
67
|
-
"-DBUILD_SHARED_LIBS=OFF",
|
|
68
|
-
"-DBUILD_BENCHMARK=OFF",
|
|
69
|
-
"-DBUILD_TESTS=OFF",
|
|
70
|
-
"-DWITH_OPENSSL=OFF",
|
|
71
|
-
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON",
|
|
72
|
-
"-DCMAKE_BUILD_TYPE=Release",
|
|
73
|
-
]
|
|
74
|
-
system(*configure_args) or fatal("cmake configure failed. See #{BUILD_DIR}/CMakeFiles/CMakeOutput.log")
|
|
106
|
+
system(build_env, *configure_args) or
|
|
107
|
+
fatal("cmake configure failed. See #{BUILD_DIR}/CMakeFiles/CMakeOutput.log")
|
|
75
108
|
end
|
|
76
109
|
|
|
77
110
|
jobs = ENV.fetch("MAKE_JOBS") { Etc.nprocessors.to_s }
|
|
78
|
-
system("cmake", "--build", BUILD_DIR, "--parallel", jobs) or fatal("cmake build failed")
|
|
111
|
+
system(build_env, "cmake", "--build", BUILD_DIR, "--parallel", jobs) or fatal("cmake build failed")
|
|
79
112
|
|
|
80
113
|
inc_dirs = [
|
|
81
114
|
VENDOR,
|
|
@@ -93,6 +126,12 @@ $CXXFLAGS = "#{$CXXFLAGS} -std=c++17 #{inc_dirs.map { |d| "-I#{d}" }.join(' ')}"
|
|
|
93
126
|
$CPPFLAGS = "#{$CPPFLAGS} #{inc_dirs.map { |d| "-I#{d}" }.join(' ')}"
|
|
94
127
|
$LDFLAGS = "#{$LDFLAGS} #{ordered_libs.map(&:shellescape).join(' ')}"
|
|
95
128
|
|
|
129
|
+
if darwin_cross
|
|
130
|
+
darwin_flags = "-mmacosx-version-min=10.15 -D_LIBCPP_DISABLE_AVAILABILITY"
|
|
131
|
+
$CXXFLAGS = "#{$CXXFLAGS} #{darwin_flags}"
|
|
132
|
+
$LDFLAGS = "#{$LDFLAGS} -mmacosx-version-min=10.15"
|
|
133
|
+
end
|
|
134
|
+
|
|
96
135
|
have_library("c++") || have_library("stdc++")
|
|
97
136
|
|
|
98
137
|
$objs = ["client.o"]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ClickhouseNative
|
|
2
4
|
class Client
|
|
3
5
|
attr_reader :host, :port, :database
|
|
@@ -13,32 +15,20 @@ module ClickhouseNative
|
|
|
13
15
|
# columns defaults to the first hash's keys (for Array<Hash>) or all table
|
|
14
16
|
# columns in DDL order (for Array<Array>).
|
|
15
17
|
# types may be supplied to skip the DESCRIBE lookup.
|
|
18
|
+
#
|
|
19
|
+
# Hash keys not present in the schema raise ArgumentError — if you need
|
|
20
|
+
# to insert a subset, pass `columns:` explicitly.
|
|
16
21
|
def insert(table, rows, columns: nil, db_name: nil, types: nil)
|
|
17
22
|
return 0 if rows.empty?
|
|
18
|
-
fq = db_name ? "#{db_name}.#{table}" : table
|
|
19
|
-
|
|
20
|
-
if types && columns
|
|
21
|
-
raise ArgumentError, "types and columns must have the same length" if columns.size != types.size
|
|
22
|
-
col_pairs = columns.zip(types).map { |n, t| [n.to_s, t] }
|
|
23
|
-
else
|
|
24
|
-
schema = describe_table(table, db_name: db_name)
|
|
25
|
-
type_by_name = schema.to_h { |c| [c[:name], c[:type]] }
|
|
26
|
-
columns ||= rows.first.is_a?(Hash) ? rows.first.keys.map(&:to_s) : schema.map { |c| c[:name] }
|
|
27
|
-
col_pairs = columns.map do |name|
|
|
28
|
-
name_s = name.to_s
|
|
29
|
-
t = type_by_name[name_s] or raise ArgumentError, "unknown column #{name_s.inspect} in #{fq}"
|
|
30
|
-
[name_s, t]
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end
|
|
24
|
+
fq = db_name ? "#{db_name}.#{table}" : table
|
|
25
|
+
col_pairs =
|
|
26
|
+
if types && columns
|
|
27
|
+
zip_columns_and_types(columns, types)
|
|
39
28
|
else
|
|
40
|
-
rows
|
|
29
|
+
columns_from_schema(table, rows, columns, db_name, fq)
|
|
41
30
|
end
|
|
31
|
+
row_arrays = rows.first.is_a?(Hash) ? hash_rows_to_arrays(rows, col_pairs) : rows
|
|
42
32
|
|
|
43
33
|
insert_block(fq, col_pairs, row_arrays)
|
|
44
34
|
end
|
|
@@ -46,5 +36,31 @@ module ClickhouseNative
|
|
|
46
36
|
def inspect
|
|
47
37
|
"#<#{self.class} #{host}:#{port}/#{database}>"
|
|
48
38
|
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def zip_columns_and_types(columns, types)
|
|
43
|
+
if columns.size != types.size
|
|
44
|
+
raise ArgumentError, "types and columns must have the same length"
|
|
45
|
+
end
|
|
46
|
+
columns.zip(types).map { |n, t| [n.to_s, t] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def columns_from_schema(table, rows, columns, db_name, fqn)
|
|
50
|
+
schema = describe_table(table, db_name: db_name)
|
|
51
|
+
type_by_name = schema.to_h { |c| [c[:name], c[:type]] }
|
|
52
|
+
columns ||= rows.first.is_a?(Hash) ? rows.first.keys.map(&:to_s) : schema.map { |c| c[:name] }
|
|
53
|
+
columns.map do |name|
|
|
54
|
+
name_s = name.to_s
|
|
55
|
+
t = type_by_name[name_s] or
|
|
56
|
+
raise ArgumentError, "unknown column #{name_s.inspect} in #{fqn}"
|
|
57
|
+
[name_s, t]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hash_rows_to_arrays(rows, col_pairs)
|
|
62
|
+
lookup = col_pairs.map { |n, _| [n.to_sym, n] }
|
|
63
|
+
rows.map { |h| lookup.map { |sym, str| h.fetch(sym) { h[str] } } }
|
|
64
|
+
end
|
|
49
65
|
end
|
|
50
66
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseNative
|
|
4
|
+
# Sequel-style logging wrapper. Prepended onto Client so it fires for the
|
|
5
|
+
# raw C-level execute/query/query_value/query_each/insert_block calls.
|
|
6
|
+
#
|
|
7
|
+
# client = Client.new(..., logger: Rails.logger)
|
|
8
|
+
# client.query("SELECT 1")
|
|
9
|
+
# # => DEBUG -- : (0.421ms) SELECT 1
|
|
10
|
+
#
|
|
11
|
+
# Errors are logged at ERROR with the elapsed time and exception class.
|
|
12
|
+
module Logging
|
|
13
|
+
LEVEL = :debug
|
|
14
|
+
|
|
15
|
+
def execute(sql)
|
|
16
|
+
log_sql(sql) { super }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def query(sql)
|
|
20
|
+
log_sql(sql) { super }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def query_value(sql)
|
|
24
|
+
log_sql(sql) { super }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def query_each(sql, &)
|
|
28
|
+
log_sql(sql) { super }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def insert_block(table, columns, rows)
|
|
32
|
+
col_list = columns.map(&:first).join(", ")
|
|
33
|
+
log_sql("INSERT INTO #{table} (#{col_list}) VALUES (#{rows.size} rows)") { super }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def log_sql(sql)
|
|
39
|
+
return yield unless @logger
|
|
40
|
+
|
|
41
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
+
begin
|
|
43
|
+
result = yield
|
|
44
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000
|
|
45
|
+
@logger.public_send(LEVEL, format("(%<ms>.3fms) %<sql>s", ms: elapsed_ms, sql: sql))
|
|
46
|
+
result
|
|
47
|
+
rescue => error
|
|
48
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000
|
|
49
|
+
first_line = error.message.to_s.lines.first.to_s.strip
|
|
50
|
+
@logger.error(format(
|
|
51
|
+
"(%<ms>.3fms) %<class>s: %<msg>s -- %<sql>s",
|
|
52
|
+
ms: elapsed_ms, class: error.class, msg: first_line, sql: sql,
|
|
53
|
+
))
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Client
|
|
60
|
+
attr_accessor :logger
|
|
61
|
+
|
|
62
|
+
prepend Logging
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "connection_pool"
|
|
2
4
|
|
|
3
5
|
module ClickhouseNative
|
|
4
6
|
class Pool
|
|
5
7
|
def initialize(host:, port:, database: "default", user: "default", password: "",
|
|
6
|
-
compression: :none, pool_size: 5, pool_timeout: 5)
|
|
7
|
-
client_kwargs = {host:, port:, database:, user:, password:, compression:}
|
|
8
|
+
compression: :none, logger: nil, pool_size: 5, pool_timeout: 5)
|
|
9
|
+
client_kwargs = { host:, port:, database:, user:, password:, compression:, logger: }
|
|
8
10
|
@pool = ConnectionPool.new(size: pool_size, timeout: pool_timeout) do
|
|
9
11
|
Client.new(**client_kwargs)
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
def with(&
|
|
14
|
-
@pool.with(&
|
|
15
|
+
def with(&)
|
|
16
|
+
@pool.with(&)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def execute(sql)
|
data/lib/clickhouse_native.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "bigdecimal"
|
|
1
5
|
require "clickhouse_native/version"
|
|
2
6
|
require "clickhouse_native/errors"
|
|
3
7
|
require "clickhouse_native/clickhouse_native"
|
|
4
8
|
require "clickhouse_native/client"
|
|
9
|
+
require "clickhouse_native/logging"
|
|
5
10
|
require "clickhouse_native/pool"
|
|
6
11
|
|
|
7
12
|
module ClickhouseNative
|
metadata
CHANGED
|
@@ -1,84 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clickhouse-native
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuri Smirnov
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-04-22 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: connection_pool
|
|
14
15
|
requirement: !ruby/object:Gem::Requirement
|
|
15
16
|
requirements:
|
|
16
|
-
- - "
|
|
17
|
+
- - ">="
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
19
|
version: '2.4'
|
|
19
20
|
type: :runtime
|
|
20
21
|
prerelease: false
|
|
21
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
23
|
requirements:
|
|
23
|
-
- - "
|
|
24
|
+
- - ">="
|
|
24
25
|
- !ruby/object:Gem::Version
|
|
25
26
|
version: '2.4'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rake
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '13.2'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '13.2'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rake-compiler
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.2'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.2'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: rake-compiler-dock
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '1.9'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.9'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: rspec
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '3.13'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '3.13'
|
|
82
27
|
description: A high-performance Ruby client for ClickHouse using the native binary
|
|
83
28
|
protocol via a C++ extension wrapping clickhouse-cpp.
|
|
84
29
|
email:
|
|
@@ -341,14 +286,16 @@ files:
|
|
|
341
286
|
- ext/clickhouse_native/vendor/clickhouse-cpp/contrib/zstd/zstd/zstd.h
|
|
342
287
|
- ext/clickhouse_native/vendor/clickhouse-cpp/contrib/zstd/zstd/zstd_errors.h
|
|
343
288
|
- lib/clickhouse_native.rb
|
|
344
|
-
- lib/clickhouse_native/clickhouse_native.bundle
|
|
345
289
|
- lib/clickhouse_native/client.rb
|
|
346
290
|
- lib/clickhouse_native/errors.rb
|
|
291
|
+
- lib/clickhouse_native/logging.rb
|
|
347
292
|
- lib/clickhouse_native/pool.rb
|
|
348
293
|
- lib/clickhouse_native/version.rb
|
|
294
|
+
homepage:
|
|
349
295
|
licenses:
|
|
350
296
|
- Apache-2.0
|
|
351
297
|
metadata: {}
|
|
298
|
+
post_install_message:
|
|
352
299
|
rdoc_options: []
|
|
353
300
|
require_paths:
|
|
354
301
|
- lib
|
|
@@ -356,14 +303,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
356
303
|
requirements:
|
|
357
304
|
- - ">="
|
|
358
305
|
- !ruby/object:Gem::Version
|
|
359
|
-
version: 3.
|
|
306
|
+
version: 3.3.0
|
|
360
307
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
361
308
|
requirements:
|
|
362
309
|
- - ">="
|
|
363
310
|
- !ruby/object:Gem::Version
|
|
364
311
|
version: '0'
|
|
365
312
|
requirements: []
|
|
366
|
-
rubygems_version:
|
|
313
|
+
rubygems_version: 3.5.22
|
|
314
|
+
signing_key:
|
|
367
315
|
specification_version: 4
|
|
368
316
|
summary: ClickHouse Ruby driver over the native TCP protocol
|
|
369
317
|
test_files: []
|
|
Binary file
|