clickhouse-native 0.11.1 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbc7f982fc96317d6b6721bc7162e22d01eac4f4fc9edb307443d24fdee59032
4
- data.tar.gz: 824150b24641414bfc0ccc4a6e37aa521b768bb73423c1bda9c1bfbefc466044
3
+ metadata.gz: d4bbe836a1758637fb9dd0a157505037b8bc45d42d253d261b3a349cecaf9716
4
+ data.tar.gz: 7c0cd5bbea6944c2c0672780088d4342cdf207227bce2c52f388986e9b26bf15
5
5
  SHA512:
6
- metadata.gz: 6cda8a41d58816d63aa064ae3008b20980d35be6f695ff04433bc760ec28c0a286b707bdeb725d5691ac8245533c05d90df4c9c5b3eb13af6b860a179d9349db
7
- data.tar.gz: caf00f391e65eb920a2d917316fa05c1569b7aac7c89f83a122b149c3305a3b0b40b6dc02afa5bd1cebcd78f0897700d5a2f9fbafb2c0d327da973dd3bc542f6
6
+ metadata.gz: e3abc7862bbf8125ca8168d5a28337ebdf1e11c1f8a2965fff597ccaa4d27eaea70f2116d93b54d2822f6f6fd3fca237e8a864d1cf30e91c0195bb4e730f22be
7
+ data.tar.gz: 101b7ecc586641e3c0df0f14f0dbd977a450d578e029fbc220f1b86218d4e84ef4a22d5c29eaec1c1e463db96e5fdc9a2b26fb5efdfa0cf1c6780a40aee1d047
@@ -804,6 +804,64 @@ static void apply_default_settings(Query& q, CHClient* c) {
804
804
  }
805
805
  }
806
806
 
807
+ // ------------------------------------------------------------------
808
+ // Connect / reconnect, off the GVL
809
+ // ------------------------------------------------------------------
810
+
811
+ namespace {
812
+ struct ResetNoGVL {
813
+ Client* client;
814
+ std::exception_ptr err;
815
+ bool ran;
816
+ };
817
+
818
+ struct ConnectNoGVL {
819
+ const ClientOptions* opts;
820
+ std::unique_ptr<Client>* out;
821
+ std::exception_ptr err;
822
+ };
823
+ } // namespace
824
+
825
+ static void* connect_no_gvl(void* data) {
826
+ auto* a = static_cast<ConnectNoGVL*>(data);
827
+ try { *a->out = std::make_unique<Client>(*a->opts); }
828
+ catch (...) { a->err = std::current_exception(); }
829
+ return nullptr;
830
+ }
831
+
832
+ static void* reset_no_gvl(void* data) {
833
+ auto* a = static_cast<ResetNoGVL*>(data);
834
+ a->ran = true;
835
+ try { a->client->ResetConnection(); } catch (...) { a->err = std::current_exception(); }
836
+ return nullptr;
837
+ }
838
+
839
+ // ResetConnection() opens a fresh socket and re-handshakes, so it blocks on the
840
+ // network exactly like a query does. Every error path calls it, which is when
841
+ // the server is least likely to answer promptly - the worst moment to be
842
+ // holding the GVL. No unblock function: there is no in-flight query to cancel,
843
+ // and the half-built connection is this thread's.
844
+ //
845
+ // _gvl2, not the ordinary one: every caller is already reporting some other
846
+ // error, and the ordinary variant checks pending interrupts as it retakes the
847
+ // GVL and can longjmp out. That would jump between an rb_protect tag and the
848
+ // rb_jump_tag meant to re-raise it, or out of the active catch handler in
849
+ // insert_block, and would lose the error the caller came here to report.
850
+ //
851
+ // _gvl2 declines to run `func` at all when an interrupt is already pending,
852
+ // though, so the reset is not guaranteed - and one caller needs it to happen:
853
+ // the `break` out of query_each keeps its client, on the understanding that
854
+ // the reset here left it clean (see Pool#discard_unless_clean). Falling back
855
+ // to the plain call covers that. Holding the GVL for a reconnect is the lesser
856
+ // evil against handing back a connection with a reply still draining into it.
857
+ // Failures stay swallowed either way: the pool discards on every other path.
858
+ static void reset_connection_no_gvl(Client* client) {
859
+ if (!client) return;
860
+ ResetNoGVL args{client, nullptr, false};
861
+ rb_thread_call_without_gvl2(reset_no_gvl, &args, nullptr, nullptr);
862
+ if (!args.ran) { try { client->ResetConnection(); } catch (...) {} }
863
+ }
864
+
807
865
  // Client.new(host:, port:, database:, user:, password:)
808
866
  static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
809
867
  VALUE kwargs = Qnil;
@@ -838,7 +896,7 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
838
896
  reinterpret_cast<VALUE>(&c->default_settings));
839
897
  }
840
898
 
841
- try {
899
+ {
842
900
  ClientOptions opts;
843
901
  opts.SetHost(host).SetPort(port)
844
902
  .SetDefaultDatabase(database).SetUser(user).SetPassword(password)
@@ -846,9 +904,17 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
846
904
  .SetPingBeforeQuery(ping_before_query)
847
905
  .TcpKeepAlive(tcp_keepalive)
848
906
  .SetRetryTimeout(std::chrono::seconds(retry_timeout));
849
- c->client = std::make_unique<Client>(opts);
850
- } catch (const std::exception& e) {
851
- raise_mapped_ex(e);
907
+ // The constructor connects and handshakes, and retries for
908
+ // SetRetryTimeout seconds against an unreachable host - long enough to
909
+ // freeze every other thread if it ran under the GVL. No unblock
910
+ // function: there is no in-flight query to cancel, and the socket being
911
+ // built belongs to this thread.
912
+ ConnectNoGVL args{&opts, &c->client, nullptr};
913
+ rb_thread_call_without_gvl(connect_no_gvl, &args, nullptr, nullptr);
914
+ if (args.err) {
915
+ try { std::rethrow_exception(args.err); }
916
+ catch (const std::exception& e) { raise_mapped_ex(e); }
917
+ }
852
918
  }
853
919
 
854
920
  rb_ivar_set(self, rb_intern("@host"), rb_utf8_str_new(host.data(), host.size()));
@@ -922,7 +988,7 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
922
988
  // so this covers what is left: an unblock function that fires without a
923
989
  // longjmp behind it, from a trap handler that does not raise or an
924
990
  // interrupt Thread.handle_interrupt has deferred.
925
- if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
991
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
926
992
  try { std::rethrow_exception(args.err); }
927
993
  catch (const std::exception& e) { raise_mapped_ex(e); }
928
994
  }
@@ -930,82 +996,171 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
930
996
  }
931
997
 
932
998
  // ------------------------------------------------------------------
933
- // query() — buffers all rows into an array; GVL is held for the duration.
934
- // See query_each below for a streaming, GVL-releasing variant.
999
+ // query() / query_value() buffer the result, releasing the GVL while the
1000
+ // server streams. The data callback is the only part that needs Ruby, so it
1001
+ // takes the GVL back per block (as query_each does) rather than the whole
1002
+ // call holding it: a multi-second read otherwise stops every other thread in
1003
+ // the process, which starves timers and lock heartbeats.
935
1004
  // ------------------------------------------------------------------
936
1005
 
937
- static VALUE ch_client_query(int argc, VALUE* argv, VALUE self) {
938
- VALUE rb_sql, kwargs = Qnil;
939
- rb_scan_args(argc, argv, "1:", &rb_sql, &kwargs);
940
- Check_Type(rb_sql, T_STRING);
941
- CHClient* c = as_client(self);
942
- if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
1006
+ namespace {
1007
+ struct CollectState {
1008
+ VALUE rows; // Qnil for query_value, which keeps `out` instead
1009
+ VALUE out;
1010
+ bool first_only;
1011
+ bool seen;
1012
+ std::vector<ID> col_ids;
1013
+ int exc_tag;
1014
+ std::exception_ptr cpp_err;
1015
+ bool aborted;
1016
+ };
1017
+
1018
+ struct CollectBlockArgs {
1019
+ const Block* block;
1020
+ CollectState* state;
1021
+ };
1022
+
1023
+ struct QueryNoGVL {
1024
+ Client* client;
1025
+ const Query* query;
1026
+ CollectState* state;
1027
+ std::exception_ptr err;
1028
+ bool cancelled;
1029
+ };
1030
+ } // namespace
1031
+
1032
+ // The decode throw is caught here, inside the frame rb_protect calls, so it
1033
+ // never unwinds through rb_protect itself - doing that walks a setjmp frame
1034
+ // whose epilogue never runs and takes the interpreter down. Carried out via
1035
+ // state instead; run_buffered_query rethrows it after the no-GVL region.
1036
+ static VALUE collect_rows_body(VALUE arg) {
1037
+ auto* args = reinterpret_cast<CollectBlockArgs*>(arg);
1038
+ const Block& block = *args->block;
1039
+ auto* state = args->state;
943
1040
 
944
- VALUE rows = rb_ary_new();
945
1041
  try {
946
- std::vector<ID> col_ids;
947
- Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
948
- apply_default_settings(q, c);
949
- apply_read_settings(q, kwargs);
950
- q.OnData([&](const Block& block) {
951
- size_t ncols = block.GetColumnCount();
952
- size_t nrows = block.GetRowCount();
953
- if (nrows == 0) return;
954
- if (col_ids.empty()) {
955
- col_ids.reserve(ncols);
956
- for (size_t i = 0; i < ncols; i++) {
957
- const std::string& name = block.GetColumnName(i);
958
- col_ids.push_back(rb_intern2(name.data(), name.size()));
959
- }
1042
+ size_t ncols = block.GetColumnCount();
1043
+ size_t nrows = block.GetRowCount();
1044
+ if (nrows == 0 || ncols == 0) return Qnil;
1045
+
1046
+ if (state->first_only) {
1047
+ if (!state->seen) {
1048
+ state->out = value_at(block[0], 0, block.GetColumnType(0));
1049
+ state->seen = true;
960
1050
  }
961
- for (size_t r = 0; r < nrows; r++) {
962
- VALUE h = rb_hash_new();
963
- for (size_t cc = 0; cc < ncols; cc++) {
964
- rb_hash_aset(h, ID2SYM(col_ids[cc]),
965
- value_at(block[cc], r, block.GetColumnType(cc)));
966
- }
967
- rb_ary_push(rows, h);
1051
+ return Qnil;
1052
+ }
1053
+
1054
+ if (state->col_ids.empty()) {
1055
+ state->col_ids.reserve(ncols);
1056
+ for (size_t i = 0; i < ncols; i++) {
1057
+ const std::string& name = block.GetColumnName(i);
1058
+ state->col_ids.push_back(rb_intern2(name.data(), name.size()));
968
1059
  }
969
- });
970
- c->client->Execute(q);
971
- return rows;
972
- } catch (const std::exception& e) {
973
- try { c->client->ResetConnection(); } catch (...) {}
974
- raise_mapped_ex(e);
1060
+ }
1061
+ for (size_t r = 0; r < nrows; r++) {
1062
+ VALUE h = rb_hash_new();
1063
+ for (size_t cc = 0; cc < ncols; cc++) {
1064
+ rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1065
+ value_at(block[cc], r, block.GetColumnType(cc)));
1066
+ }
1067
+ rb_ary_push(state->rows, h);
1068
+ }
1069
+ } catch (...) {
1070
+ state->cpp_err = std::current_exception();
1071
+ state->aborted = true;
975
1072
  }
976
1073
  return Qnil;
977
1074
  }
978
1075
 
979
- // ------------------------------------------------------------------
980
- // query_value returns the first cell of the first row, or nil
981
- // ------------------------------------------------------------------
1076
+ static void* with_gvl_collect(void* data) {
1077
+ auto* args = static_cast<CollectBlockArgs*>(data);
1078
+ int tag = 0;
1079
+ rb_protect(collect_rows_body, reinterpret_cast<VALUE>(args), &tag);
1080
+ if (tag != 0) {
1081
+ args->state->exc_tag = tag;
1082
+ args->state->aborted = true;
1083
+ }
1084
+ return nullptr;
1085
+ }
982
1086
 
983
- static VALUE ch_client_query_value(int argc, VALUE* argv, VALUE self) {
1087
+ static void* query_no_gvl(void* data) {
1088
+ auto* a = static_cast<QueryNoGVL*>(data);
1089
+ try {
1090
+ a->client->Execute(*a->query);
1091
+ } catch (...) {
1092
+ a->err = std::current_exception();
1093
+ }
1094
+ return nullptr;
1095
+ }
1096
+
1097
+ // See execute_unblock: cancel the in-flight query, never tear the streams down
1098
+ // from another thread.
1099
+ static void query_unblock(void* data) {
1100
+ auto* a = static_cast<QueryNoGVL*>(data);
1101
+ a->state->aborted = true;
1102
+ a->cancelled = true;
1103
+ try { a->client->CancelInFlight(); } catch (...) {}
1104
+ }
1105
+
1106
+ static VALUE run_buffered_query(VALUE self, int argc, VALUE* argv, bool first_only) {
984
1107
  VALUE rb_sql, kwargs = Qnil;
985
1108
  rb_scan_args(argc, argv, "1:", &rb_sql, &kwargs);
986
1109
  Check_Type(rb_sql, T_STRING);
987
1110
  CHClient* c = as_client(self);
988
1111
  if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
989
1112
 
990
- try {
991
- VALUE out = Qnil;
992
- bool seen = false;
993
- Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
994
- apply_default_settings(q, c);
995
- apply_read_settings(q, kwargs);
996
- q.OnData([&](const Block& block) {
997
- if (seen) return;
998
- if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) return;
999
- out = value_at(block[0], 0, block.GetColumnType(0));
1000
- seen = true;
1001
- });
1002
- c->client->Execute(q);
1003
- return out;
1004
- } catch (const std::exception& e) {
1005
- try { c->client->ResetConnection(); } catch (...) {}
1006
- raise_mapped_ex(e);
1113
+ // `state` lives on this thread's machine stack, and that is what marks
1114
+ // state.rows and state.out while the GVL is dropped below: the GC scans the
1115
+ // stack conservatively, and the context saved at the release covers the
1116
+ // no-GVL window. Moving CollectState off the stack would remove the only
1117
+ // thing keeping the result alive.
1118
+ CollectState state{first_only ? Qnil : rb_ary_new(), Qnil,
1119
+ first_only, false, {}, 0, nullptr, false};
1120
+ Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
1121
+ apply_default_settings(q, c);
1122
+ apply_read_settings(q, kwargs);
1123
+ q.OnDataCancelable([&state](const Block& block) -> bool {
1124
+ if (state.aborted) return false;
1125
+ // query_value keeps reading to leave the connection clean, but has
1126
+ // nothing left to do once it holds its cell - taking the GVL per block
1127
+ // to discard it costs a round trip each time on a large result.
1128
+ if (state.first_only && state.seen) return true;
1129
+ CollectBlockArgs ca{&block, &state};
1130
+ rb_thread_call_with_gvl(with_gvl_collect, &ca);
1131
+ return !state.aborted;
1132
+ });
1133
+ QueryNoGVL args{c->client.get(), &q, &state, nullptr, false};
1134
+
1135
+ rb_thread_call_without_gvl(query_no_gvl, &args, query_unblock, &args);
1136
+
1137
+ // A decode throw caught in the data callback is the real error; the read
1138
+ // that follows it only reports the abort, so this is checked first.
1139
+ if (state.cpp_err) {
1140
+ reset_connection_no_gvl(c->client.get());
1141
+ try { std::rethrow_exception(state.cpp_err); }
1142
+ catch (const std::exception& e) { raise_mapped_ex(e); }
1007
1143
  }
1008
- return Qnil;
1144
+ if (args.err) {
1145
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1146
+ if (state.exc_tag) rb_jump_tag(state.exc_tag);
1147
+ try { std::rethrow_exception(args.err); }
1148
+ catch (const std::exception& e) { raise_mapped_ex(e); }
1149
+ }
1150
+ if (state.exc_tag) {
1151
+ reset_connection_no_gvl(c->client.get());
1152
+ rb_jump_tag(state.exc_tag);
1153
+ }
1154
+
1155
+ return first_only ? state.out : state.rows;
1156
+ }
1157
+
1158
+ static VALUE ch_client_query(int argc, VALUE* argv, VALUE self) {
1159
+ return run_buffered_query(self, argc, argv, false);
1160
+ }
1161
+
1162
+ static VALUE ch_client_query_value(int argc, VALUE* argv, VALUE self) {
1163
+ return run_buffered_query(self, argc, argv, true);
1009
1164
  }
1010
1165
 
1011
1166
  // ------------------------------------------------------------------
@@ -1096,12 +1251,12 @@ static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns
1096
1251
  InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr, false};
1097
1252
  rb_thread_call_without_gvl(insert_no_gvl, &args, insert_unblock, &args);
1098
1253
  if (args.err) {
1099
- if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1254
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1100
1255
  try { std::rethrow_exception(args.err); }
1101
1256
  catch (const std::exception& e) { raise_mapped_ex(e); }
1102
1257
  }
1103
1258
  } catch (const std::exception& e) {
1104
- try { c->client->ResetConnection(); } catch (...) {}
1259
+ reset_connection_no_gvl(c->client.get());
1105
1260
  raise_mapped_ex(e);
1106
1261
  }
1107
1262
  return LONG2NUM(nrows);
@@ -1116,6 +1271,7 @@ struct QueryEachState {
1116
1271
  VALUE user_proc;
1117
1272
  std::vector<ID> col_ids;
1118
1273
  int exc_tag;
1274
+ std::exception_ptr cpp_err;
1119
1275
  bool aborted;
1120
1276
  };
1121
1277
 
@@ -1133,27 +1289,35 @@ struct QueryEachNoGVL {
1133
1289
  };
1134
1290
  } // namespace
1135
1291
 
1292
+ // See collect_rows_body: the decode throw is caught inside this frame so it
1293
+ // never unwinds through rb_protect.
1136
1294
  static VALUE yield_rows_body(VALUE arg) {
1137
1295
  auto* args = reinterpret_cast<YieldBlockArgs*>(arg);
1138
1296
  const Block& block = *args->block;
1139
1297
  auto* state = args->state;
1140
- size_t ncols = block.GetColumnCount();
1141
- size_t nrows = block.GetRowCount();
1142
- if (nrows == 0) return Qnil;
1143
- if (state->col_ids.empty() && ncols > 0) {
1144
- state->col_ids.reserve(ncols);
1145
- for (size_t i = 0; i < ncols; i++) {
1146
- const std::string& name = block.GetColumnName(i);
1147
- state->col_ids.push_back(rb_intern2(name.data(), name.size()));
1298
+
1299
+ try {
1300
+ size_t ncols = block.GetColumnCount();
1301
+ size_t nrows = block.GetRowCount();
1302
+ if (nrows == 0) return Qnil;
1303
+ if (state->col_ids.empty() && ncols > 0) {
1304
+ state->col_ids.reserve(ncols);
1305
+ for (size_t i = 0; i < ncols; i++) {
1306
+ const std::string& name = block.GetColumnName(i);
1307
+ state->col_ids.push_back(rb_intern2(name.data(), name.size()));
1308
+ }
1148
1309
  }
1149
- }
1150
- for (size_t r = 0; r < nrows; r++) {
1151
- VALUE h = rb_hash_new();
1152
- for (size_t cc = 0; cc < ncols; cc++) {
1153
- rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1154
- value_at(block[cc], r, block.GetColumnType(cc)));
1310
+ for (size_t r = 0; r < nrows; r++) {
1311
+ VALUE h = rb_hash_new();
1312
+ for (size_t cc = 0; cc < ncols; cc++) {
1313
+ rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1314
+ value_at(block[cc], r, block.GetColumnType(cc)));
1315
+ }
1316
+ rb_funcall(state->user_proc, rb_intern("call"), 1, h);
1155
1317
  }
1156
- rb_funcall(state->user_proc, rb_intern("call"), 1, h);
1318
+ } catch (...) {
1319
+ state->cpp_err = std::current_exception();
1320
+ state->aborted = true;
1157
1321
  }
1158
1322
  return Qnil;
1159
1323
  }
@@ -1194,7 +1358,7 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1194
1358
  CHClient* c = as_client(self);
1195
1359
  if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
1196
1360
 
1197
- QueryEachState state{rb_block_proc(), {}, 0, false};
1361
+ QueryEachState state{rb_block_proc(), {}, 0, nullptr, false};
1198
1362
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
1199
1363
  apply_default_settings(q, c);
1200
1364
  apply_read_settings(q, kwargs);
@@ -1208,14 +1372,21 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1208
1372
 
1209
1373
  rb_thread_call_without_gvl(query_each_no_gvl, &args, query_each_unblock, &args);
1210
1374
 
1375
+ // A decode throw caught in the data callback is the real error; the read
1376
+ // that follows it only reports the abort, so this is checked first.
1377
+ if (state.cpp_err) {
1378
+ reset_connection_no_gvl(c->client.get());
1379
+ try { std::rethrow_exception(state.cpp_err); }
1380
+ catch (const std::exception& e) { raise_mapped_ex(e); }
1381
+ }
1211
1382
  if (args.err) {
1212
- if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1383
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1213
1384
  if (state.exc_tag) rb_jump_tag(state.exc_tag);
1214
1385
  try { std::rethrow_exception(args.err); }
1215
1386
  catch (const std::exception& e) { raise_mapped_ex(e); }
1216
1387
  }
1217
1388
  if (state.exc_tag) {
1218
- try { c->client->ResetConnection(); } catch (...) {}
1389
+ reset_connection_no_gvl(c->client.get());
1219
1390
  rb_jump_tag(state.exc_tag);
1220
1391
  }
1221
1392
  return self;
@@ -1270,7 +1441,7 @@ static VALUE ch_client_server_version(VALUE self) {
1270
1441
  static VALUE ch_client_reset_connection(VALUE self) {
1271
1442
  CHClient* c = as_client(self);
1272
1443
  if (!c->client) return Qnil;
1273
- try { c->client->ResetConnection(); } catch (...) {}
1444
+ reset_connection_no_gvl(c->client.get());
1274
1445
  return Qtrue;
1275
1446
  }
1276
1447
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClickhouseNative
4
- VERSION = "0.11.1"
4
+ VERSION = "0.12.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clickhouse-native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Smirnov