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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80f06499c2088ed7d221e22eb2286da9db531eeb0322b94c65333cbeb6f0ecc6
4
- data.tar.gz: 9ebf8a7b867cd4ca90aedb7d96f62f56e5121ac5f5bd36808a947a9ffe57cfc4
3
+ metadata.gz: 51aa32b5ec77367fcc08b8df05c2449bb8e99b7f1cc3b7cc331d83c408cea32c
4
+ data.tar.gz: 3b28a9d0cde5d7c827f212bb5ecde1d0dee21f5a8e4cc2faaa20de06534f59d5
5
5
  SHA512:
6
- metadata.gz: bd5bfef33530626520a66fee2e14c4d1e07cbc9d6483cb6c0e8f53e65e93f5a14328bea4dbf08e35839227e4376fe0c67660d8b74e5f8d9c9503be570735e2c6
7
- data.tar.gz: a9dc31d3f8ac4dd2b4d24d74847035917509604861b60e668b93a37cdcddd93de66ae086c45feb05052242679c21b1d2e1eb28afd3d1e6df65998a8b14d56d2b
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 exception used to tag encoder failures and drive them through
34
- // raise_mapped_ex -> err_encoder without rb_raising from inside a try block.
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
- auto sv = lc->GetItem(idx).AsBinaryData();
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
- rb_raise(err_decoder, "clickhouse-native: malformed Map column");
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
- rb_raise(err_unsupported,
192
- "clickhouse-native: unsupported column type %s (code=%d)",
193
- type->GetName().c_str(), static_cast<int>(type->GetCode()));
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 to_i = rb_funcall(value, rb_intern("to_i"), 0);
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 to_i = rb_funcall(value, rb_intern("to_i"), 0);
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 to_i = rb_funcall(value, rb_intern("to_i"), 0);
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
- col->As<ColumnDateTime64>()->Append(time_to_datetime64_ticks(value, prec));
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() — synchronous, GVL held (streaming query_each comes in Week 5)
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
- BUILD_DIR = File.expand_path("build", EXT_DIR)
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
- row_arrays =
35
- if rows.first.is_a?(Hash)
36
- col_pairs.map { |n, _| [n.to_sym, n] }.then do |lookup|
37
- rows.map { |h| lookup.map { |sym, str| h.fetch(sym) { h[str] } } }
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClickhouseNative
2
4
  class Error < StandardError; end
3
5
 
@@ -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(&block)
14
- @pool.with(&block)
15
+ def with(&)
16
+ @pool.with(&)
15
17
  end
16
18
 
17
19
  def execute(sql)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClickhouseNative
2
- VERSION = "0.0.1"
4
+ VERSION = "0.1.1"
3
5
  end
@@ -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.0.1
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: 1980-01-02 00:00:00.000000000 Z
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.1.0
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: 4.0.6
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: []