mysql2 0.1.4 → 0.1.5

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.
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 (May 12th, 2010)
4
+ * quite a few patches from Eric Wong related to thread-safety, non-blocking I/O and general cleanup
5
+ ** wrap mysql_real_connect with rb_thread_blocking_region
6
+ ** release GVL for possibly blocking mysql_* library calls
7
+ ** [cleanup] quiet down warnings
8
+ ** [cleanup] make all C symbols static
9
+ ** add Mysql2::Client#close method
10
+ ** correctly free the wrapped result in case of EOF
11
+ ** Fix memory leak from the result wrapper struct itself
12
+ ** make Mysql2::Client destructor safely non-blocking
13
+ * bug fixes for ActiveRecord adapter
14
+ ** added casting for default values since they all come back from Mysql as strings (!?!)
15
+ ** missing constant was added
16
+ ** fixed a typo in the show_variable method
17
+ * switched over sscanf for date/time parsing in C
18
+ * made some specs a little finer-grained
19
+ * initial Sequel adapter added
20
+ * updated query benchmarks to reflect the difference between casting in C and in Ruby
21
+
3
22
  ## 0.1.4 (April 23rd, 2010)
4
23
  * optimization: implemented a local cache for rows that are lazily created in ruby during iteration. The MySQL C result is freed as soon as all the results have been cached
5
24
  * optimization: implemented a local cache for field names so every row reuses the same objects as field names/keys
@@ -130,11 +130,11 @@ Me: Yep, but it's API is considerably more complex *and* is 2-3x slower.
130
130
  Performing a basic "SELECT * FROM" query on a table with 30k rows and fields of nearly every Ruby-representable data type,
131
131
  then iterating over every row using an #each like method yielding a block:
132
132
 
133
- # And remember, the Mysql gem only gives back nil and strings for values.
133
+ # These results are from the query_with_mysql_casting.rb script in the benchmarks folder
134
134
  user system total real
135
135
  Mysql2
136
- 0.610000 0.160000 0.770000 ( 0.986967)
136
+ 0.890000 0.190000 1.080000 ( 2.028887)
137
137
  Mysql
138
- 0.350000 0.220000 0.570000 ( 1.457889)
138
+ 7.330000 0.350000 7.680000 ( 8.013160)
139
139
  do_mysql
140
- 1.710000 0.180000 1.890000 ( 2.124831)
140
+ 1.740000 0.220000 1.960000 ( 2.909290)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
@@ -15,24 +15,36 @@ mysql_opts = {
15
15
  :database => 'test'
16
16
  }
17
17
 
18
- class TestModel < ActiveRecord::Base
18
+ class Mysql2Model < ActiveRecord::Base
19
+ set_table_name :mysql2_test
20
+ end
21
+
22
+ class MysqlModel < ActiveRecord::Base
19
23
  set_table_name :mysql2_test
20
24
  end
21
25
 
22
26
  Benchmark.bmbm do |x|
23
27
  x.report do
24
- TestModel.establish_connection(mysql2_opts)
28
+ Mysql2Model.establish_connection(mysql2_opts)
25
29
  puts "Mysql2"
26
30
  number_of.times do
27
- TestModel.all(:limit => 1000)
31
+ Mysql2Model.all(:limit => 1000).each{ |r|
32
+ r.attributes.keys.each{ |k|
33
+ r.send(k.to_sym)
34
+ }
35
+ }
28
36
  end
29
37
  end
30
38
 
31
39
  x.report do
32
- TestModel.establish_connection(mysql_opts)
40
+ MysqlModel.establish_connection(mysql_opts)
33
41
  puts "Mysql"
34
42
  number_of.times do
35
- TestModel.all(:limit => 1000)
43
+ MysqlModel.all(:limit => 1000).each{ |r|
44
+ r.attributes.keys.each{ |k|
45
+ r.send(k.to_sym)
46
+ }
47
+ }
36
48
  end
37
49
  end
38
50
  end
@@ -0,0 +1,82 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+ require 'benchmark'
5
+ require 'mysql'
6
+ require 'mysql2_ext'
7
+ require 'do_mysql'
8
+
9
+ number_of = 100
10
+ database = 'test'
11
+ sql = "SELECT * FROM mysql2_test LIMIT 100"
12
+
13
+ class Mysql
14
+ include Enumerable
15
+ end
16
+
17
+ def mysql_cast(type, value)
18
+ case type
19
+ when Mysql::Field::TYPE_NULL
20
+ nil
21
+ when Mysql::Field::TYPE_TINY, Mysql::Field::TYPE_SHORT, Mysql::Field::TYPE_LONG,
22
+ Mysql::Field::TYPE_INT24, Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_YEAR
23
+ value.to_i
24
+ when Mysql::Field::TYPE_DECIMAL, Mysql::Field::TYPE_NEWDECIMAL
25
+ BigDecimal.new(value)
26
+ when Mysql::Field::TYPE_DOUBLE, Mysql::Field::TYPE_FLOAT
27
+ value.to_f
28
+ when Mysql::Field::TYPE_DATE
29
+ Date.parse(value)
30
+ when Mysql::Field::TYPE_TIME, Mysql::Field::TYPE_DATETIME, Mysql::Field::TYPE_TIMESTAMP
31
+ Time.parse(value)
32
+ when Mysql::Field::TYPE_BLOB, Mysql::Field::TYPE_BIT, Mysql::Field::TYPE_STRING,
33
+ Mysql::Field::TYPE_VAR_STRING, Mysql::Field::TYPE_CHAR, Mysql::Field::TYPE_SET
34
+ Mysql::Field::TYPE_ENUM
35
+ value
36
+ else
37
+ value
38
+ end
39
+ end
40
+
41
+ Benchmark.bmbm do |x|
42
+ mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root")
43
+ mysql2.query "USE #{database}"
44
+ x.report do
45
+ puts "Mysql2"
46
+ number_of.times do
47
+ mysql2_result = mysql2.query sql
48
+ mysql2_result.each(:symbolize_keys => true) do |res|
49
+ # puts res.inspect
50
+ end
51
+ end
52
+ end
53
+
54
+ mysql = Mysql.new("localhost", "root")
55
+ mysql.query "USE #{database}"
56
+ x.report do
57
+ puts "Mysql"
58
+ number_of.times do
59
+ mysql_result = mysql.query sql
60
+ fields = mysql_result.fetch_fields
61
+ mysql_result.each do |row|
62
+ row_hash = {}
63
+ row.each_with_index do |f, j|
64
+ row_hash[fields[j].name.to_sym] = mysql_cast(fields[j].type, row[j])
65
+ end
66
+ # puts row_hash.inspect
67
+ end
68
+ end
69
+ end
70
+
71
+ do_mysql = DataObjects::Connection.new("mysql://localhost/#{database}")
72
+ command = DataObjects::Mysql::Command.new do_mysql, sql
73
+ x.report do
74
+ puts "do_mysql"
75
+ number_of.times do
76
+ do_result = command.execute_reader
77
+ do_result.each do |res|
78
+ # puts res.inspect
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'benchmark'
6
+ require 'sequel'
7
+ require 'sequel/adapters/do'
8
+
9
+ number_of = 10
10
+ mysql2_opts = "mysql2://localhost/test"
11
+ mysql_opts = "mysql://localhost/test"
12
+ do_mysql_opts = "do:mysql://localhost/test"
13
+
14
+ class Mysql2Model < Sequel::Model(Sequel.connect(mysql2_opts)[:mysql2_test]); end
15
+ class MysqlModel < Sequel::Model(Sequel.connect(mysql_opts)[:mysql2_test]); end
16
+ class DOMysqlModel < Sequel::Model(Sequel.connect(do_mysql_opts)[:mysql2_test]); end
17
+
18
+ Benchmark.bmbm do |x|
19
+ x.report do
20
+ puts "Mysql2"
21
+ number_of.times do
22
+ Mysql2Model.limit(1000).all
23
+ end
24
+ end
25
+
26
+ x.report do
27
+ puts "do:mysql"
28
+ number_of.times do
29
+ DOMysqlModel.limit(1000).all
30
+ end
31
+ end
32
+
33
+ x.report do
34
+ puts "Mysql"
35
+ number_of.times do
36
+ MysqlModel.limit(1000).all
37
+ end
38
+ end
39
+ end
@@ -1,6 +1,9 @@
1
1
  # encoding: UTF-8
2
2
  require 'mkmf'
3
3
 
4
+ # 1.9-only
5
+ have_func('rb_thread_blocking_region')
6
+
4
7
  # borrowed from mysqlplus
5
8
  # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb
6
9
  dirs = ENV['PATH'].split(':') + %w[
@@ -1,54 +1,105 @@
1
1
  #include "mysql2_ext.h"
2
2
 
3
+ /*
4
+ * non-blocking mysql_*() functions that we won't be wrapping since
5
+ * they do not appear to hit the network nor issue any interruptible
6
+ * or blocking system calls.
7
+ *
8
+ * - mysql_affected_rows()
9
+ * - mysql_error()
10
+ * - mysql_fetch_fields()
11
+ * - mysql_fetch_lengths() - calls cli_fetch_lengths or emb_fetch_lengths
12
+ * - mysql_field_count()
13
+ * - mysql_get_client_info()
14
+ * - mysql_get_client_version()
15
+ * - mysql_get_server_info()
16
+ * - mysql_get_server_version()
17
+ * - mysql_insert_id()
18
+ * - mysql_num_fields()
19
+ * - mysql_num_rows()
20
+ * - mysql_options()
21
+ * - mysql_real_escape_string()
22
+ * - mysql_ssl_set()
23
+ */
24
+
25
+ static VALUE nogvl_init(void *ptr) {
26
+ struct nogvl_connect_args *args = ptr;
27
+
28
+ /* may initialize embedded server and read /etc/services off disk */
29
+ args->mysql = mysql_init(NULL);
30
+
31
+ return args->mysql == NULL ? Qfalse : Qtrue;
32
+ }
33
+
34
+ static VALUE nogvl_connect(void *ptr)
35
+ {
36
+ struct nogvl_connect_args *args = ptr;
37
+ MYSQL *client;
38
+
39
+ client = mysql_real_connect(args->mysql, args->host,
40
+ args->user, args->passwd,
41
+ args->db, args->port, args->unix_socket,
42
+ args->client_flag);
43
+
44
+ return client ? Qtrue : Qfalse;
45
+ }
46
+
3
47
  /* Mysql2::Client */
4
48
  static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
5
- MYSQL * client;
49
+ mysql2_client_wrapper * client;
50
+ struct nogvl_connect_args args = {
51
+ .host = "localhost",
52
+ .user = NULL,
53
+ .passwd = NULL,
54
+ .db = NULL,
55
+ .port = 3306,
56
+ .unix_socket = NULL,
57
+ .client_flag = 0
58
+ };
6
59
  VALUE obj, opts;
7
60
  VALUE rb_host, rb_socket, rb_port, rb_database,
8
61
  rb_username, rb_password, rb_reconnect,
9
62
  rb_connect_timeout;
10
63
  VALUE rb_ssl_client_key, rb_ssl_client_cert, rb_ssl_ca_cert,
11
64
  rb_ssl_ca_path, rb_ssl_cipher;
12
- char *host = "localhost", *socket = NULL, *username = NULL,
13
- *password = NULL, *database = NULL;
14
65
  char *ssl_client_key = NULL, *ssl_client_cert = NULL, *ssl_ca_cert = NULL,
15
66
  *ssl_ca_path = NULL, *ssl_cipher = NULL;
16
- unsigned int port = 3306, connect_timeout = 0;
67
+ unsigned int connect_timeout = 0;
17
68
  my_bool reconnect = 1;
18
69
 
19
- obj = Data_Make_Struct(klass, MYSQL, NULL, rb_mysql_client_free, client);
70
+ obj = Data_Make_Struct(klass, mysql2_client_wrapper, NULL, rb_mysql_client_free, client);
20
71
 
21
72
  if (rb_scan_args(argc, argv, "01", &opts) == 1) {
22
73
  Check_Type(opts, T_HASH);
23
74
 
24
75
  if ((rb_host = rb_hash_aref(opts, sym_host)) != Qnil) {
25
76
  Check_Type(rb_host, T_STRING);
26
- host = RSTRING_PTR(rb_host);
77
+ args.host = RSTRING_PTR(rb_host);
27
78
  }
28
79
 
29
80
  if ((rb_socket = rb_hash_aref(opts, sym_socket)) != Qnil) {
30
81
  Check_Type(rb_socket, T_STRING);
31
- socket = RSTRING_PTR(rb_socket);
82
+ args.unix_socket = RSTRING_PTR(rb_socket);
32
83
  }
33
84
 
34
85
  if ((rb_port = rb_hash_aref(opts, sym_port)) != Qnil) {
35
86
  Check_Type(rb_port, T_FIXNUM);
36
- port = FIX2INT(rb_port);
87
+ args.port = FIX2INT(rb_port);
37
88
  }
38
89
 
39
90
  if ((rb_username = rb_hash_aref(opts, sym_username)) != Qnil) {
40
91
  Check_Type(rb_username, T_STRING);
41
- username = RSTRING_PTR(rb_username);
92
+ args.user = RSTRING_PTR(rb_username);
42
93
  }
43
94
 
44
95
  if ((rb_password = rb_hash_aref(opts, sym_password)) != Qnil) {
45
96
  Check_Type(rb_password, T_STRING);
46
- password = RSTRING_PTR(rb_password);
97
+ args.passwd = RSTRING_PTR(rb_password);
47
98
  }
48
99
 
49
100
  if ((rb_database = rb_hash_aref(opts, sym_database)) != Qnil) {
50
101
  Check_Type(rb_database, T_STRING);
51
- database = RSTRING_PTR(rb_database);
102
+ args.db = RSTRING_PTR(rb_database);
52
103
  }
53
104
 
54
105
  if ((rb_reconnect = rb_hash_aref(opts, sym_reconnect)) != Qnil) {
@@ -87,82 +138,145 @@ static VALUE rb_mysql_client_new(int argc, VALUE * argv, VALUE klass) {
87
138
  }
88
139
  }
89
140
 
90
- if (!mysql_init(client)) {
141
+ if (rb_thread_blocking_region(nogvl_init, &args, RUBY_UBF_IO, 0) == Qfalse) {
91
142
  // TODO: warning - not enough memory?
92
- rb_raise(cMysql2Error, "%s", mysql_error(client));
143
+ rb_raise(cMysql2Error, "%s", mysql_error(args.mysql));
93
144
  return Qnil;
94
145
  }
95
146
 
96
147
  // set default reconnect behavior
97
- if (mysql_options(client, MYSQL_OPT_RECONNECT, &reconnect) != 0) {
148
+ if (mysql_options(args.mysql, MYSQL_OPT_RECONNECT, &reconnect) != 0) {
98
149
  // TODO: warning - unable to set reconnect behavior
99
- rb_warn("%s\n", mysql_error(client));
150
+ rb_warn("%s\n", mysql_error(args.mysql));
100
151
  }
101
152
 
102
153
  // set default connection timeout behavior
103
- if (connect_timeout != 0 && mysql_options(client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout) != 0) {
154
+ if (connect_timeout != 0 && mysql_options(args.mysql, MYSQL_OPT_CONNECT_TIMEOUT, (const char *)&connect_timeout) != 0) {
104
155
  // TODO: warning - unable to set connection timeout
105
- rb_warn("%s\n", mysql_error(client));
156
+ rb_warn("%s\n", mysql_error(args.mysql));
106
157
  }
107
158
 
108
159
  // force the encoding to utf8
109
- if (mysql_options(client, MYSQL_SET_CHARSET_NAME, "utf8") != 0) {
160
+ if (mysql_options(args.mysql, MYSQL_SET_CHARSET_NAME, "utf8") != 0) {
110
161
  // TODO: warning - unable to set charset
111
- rb_warn("%s\n", mysql_error(client));
162
+ rb_warn("%s\n", mysql_error(args.mysql));
112
163
  }
113
164
 
114
165
  if (ssl_ca_cert != NULL || ssl_client_key != NULL) {
115
- mysql_ssl_set(client, ssl_client_key, ssl_client_cert, ssl_ca_cert, ssl_ca_path, ssl_cipher);
166
+ mysql_ssl_set(args.mysql, ssl_client_key, ssl_client_cert, ssl_ca_cert, ssl_ca_path, ssl_cipher);
116
167
  }
117
168
 
118
- if (mysql_real_connect(client, host, username, password, database, port, socket, 0) == NULL) {
169
+ if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) {
119
170
  // unable to connect
120
- rb_raise(cMysql2Error, "%s", mysql_error(client));
171
+ rb_raise(cMysql2Error, "%s", mysql_error(args.mysql));
121
172
  return Qnil;
122
173
  }
123
174
 
175
+ client->client = args.mysql;
176
+
124
177
  rb_obj_call_init(obj, argc, argv);
125
178
  return obj;
126
179
  }
127
180
 
128
- static VALUE rb_mysql_client_init(int argc, VALUE * argv, VALUE self) {
181
+ static VALUE rb_mysql_client_init(RB_MYSQL_UNUSED int argc, RB_MYSQL_UNUSED VALUE * argv, VALUE self) {
129
182
  return self;
130
183
  }
131
184
 
132
- void rb_mysql_client_free(void * client) {
133
- MYSQL * c = client;
134
- if (c) {
135
- mysql_close(client);
185
+ static void rb_mysql_client_free(void * ptr) {
186
+ mysql2_client_wrapper * client = ptr;
187
+
188
+ if (client->client) {
189
+ /*
190
+ * we'll send a QUIT message to the server, but that message is more of a
191
+ * formality than a hard requirement since the socket is getting shutdown
192
+ * anyways, so ensure the socket write does not block our interpreter
193
+ */
194
+ int fd = client->client->net.fd;
195
+ int flags;
196
+
197
+ if (fd >= 0) {
198
+ /*
199
+ * if the socket is dead we have no chance of blocking,
200
+ * so ignore any potential fcntl errors since they don't matter
201
+ */
202
+ flags = fcntl(fd, F_GETFL);
203
+ if (flags > 0 && !(flags & O_NONBLOCK))
204
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
205
+ }
206
+
207
+ mysql_close(client->client);
136
208
  }
209
+ xfree(ptr);
210
+ }
211
+
212
+ static VALUE nogvl_close(void * ptr) {
213
+ mysql_close((MYSQL *)ptr);
214
+ return Qnil;
215
+ }
216
+
217
+ /*
218
+ * Immediately disconnect from the server, normally the garbage collector
219
+ * will disconnect automatically when a connection is no longer needed.
220
+ * Explicitly closing this will free up server resources sooner than waiting
221
+ * for the garbage collector.
222
+ */
223
+ static VALUE rb_mysql_client_close(VALUE self) {
224
+ mysql2_client_wrapper *client;
225
+
226
+ Data_Get_Struct(self, mysql2_client_wrapper, client);
227
+
228
+ if (client->client) {
229
+ rb_thread_blocking_region(nogvl_close, client->client, RUBY_UBF_IO, 0);
230
+ client->client = NULL;
231
+ } else {
232
+ rb_raise(cMysql2Error, "already closed MySQL connection");
233
+ }
234
+ return Qnil;
235
+ }
236
+
237
+ /*
238
+ * mysql_send_query is unlikely to block since most queries are small
239
+ * enough to fit in a socket buffer, but sometimes large UPDATE and
240
+ * INSERTs will cause the process to block
241
+ */
242
+ static VALUE nogvl_send_query(void *ptr)
243
+ {
244
+ struct nogvl_send_query_args *args = ptr;
245
+ int rv;
246
+ const char *sql = RSTRING_PTR(args->sql);
247
+ long sql_len = RSTRING_LEN(args->sql);
248
+
249
+ rv = mysql_send_query(args->mysql, sql, sql_len);
250
+
251
+ return rv == 0 ? Qtrue : Qfalse;
137
252
  }
138
253
 
139
254
  static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
140
- MYSQL * client;
141
- MYSQL_RES * result;
255
+ struct nogvl_send_query_args args;
142
256
  fd_set fdset;
143
257
  int fd, retval;
144
258
  int async = 0;
145
- VALUE sql, opts;
259
+ VALUE opts;
146
260
  VALUE rb_async;
147
261
 
148
- if (rb_scan_args(argc, argv, "11", &sql, &opts) == 2) {
262
+ if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) {
149
263
  if ((rb_async = rb_hash_aref(opts, sym_async)) != Qnil) {
150
264
  async = rb_async == Qtrue ? 1 : 0;
151
265
  }
152
266
  }
153
267
 
154
- Check_Type(sql, T_STRING);
268
+ Check_Type(args.sql, T_STRING);
155
269
 
156
- GetMysql2Client(self, client);
157
- if (mysql_send_query(client, RSTRING_PTR(sql), RSTRING_LEN(sql)) != 0) {
158
- rb_raise(cMysql2Error, "%s", mysql_error(client));
270
+ GetMysql2Client(self, args.mysql);
271
+ if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) {
272
+ rb_raise(cMysql2Error, "%s", mysql_error(args.mysql));
159
273
  return Qnil;
160
274
  }
161
275
 
162
276
  if (!async) {
163
277
  // the below code is largely from do_mysql
164
278
  // http://github.com/datamapper/do
165
- fd = client->net.fd;
279
+ fd = args.mysql->net.fd;
166
280
  for(;;) {
167
281
  FD_ZERO(&fdset);
168
282
  FD_SET(fd, &fdset);
@@ -208,7 +322,7 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) {
208
322
  }
209
323
  }
210
324
 
211
- static VALUE rb_mysql_client_info(VALUE self) {
325
+ static VALUE rb_mysql_client_info(RB_MYSQL_UNUSED VALUE self) {
212
326
  VALUE version = rb_hash_new();
213
327
  rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_client_version()));
214
328
  rb_hash_aset(version, sym_version, rb_str_new2(mysql_get_client_info()));
@@ -231,17 +345,37 @@ static VALUE rb_mysql_client_socket(VALUE self) {
231
345
  return INT2NUM(client->net.fd);
232
346
  }
233
347
 
348
+ /*
349
+ * even though we did rb_thread_select before calling this, a large
350
+ * response can overflow the socket buffers and cause us to eventually
351
+ * block while calling mysql_read_query_result
352
+ */
353
+ static VALUE nogvl_read_query_result(void *ptr)
354
+ {
355
+ MYSQL * client = ptr;
356
+ my_bool res = mysql_read_query_result(client);
357
+
358
+ return res == 0 ? Qtrue : Qfalse;
359
+ }
360
+
361
+ /* mysql_store_result may (unlikely) read rows off the socket */
362
+ static VALUE nogvl_store_result(void *ptr)
363
+ {
364
+ MYSQL * client = ptr;
365
+ return (VALUE)mysql_store_result(client);
366
+ }
367
+
234
368
  static VALUE rb_mysql_client_async_result(VALUE self) {
235
369
  MYSQL * client;
236
370
  MYSQL_RES * result;
237
371
  GetMysql2Client(self, client);
238
372
 
239
- if (mysql_read_query_result(client) != 0) {
373
+ if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) {
240
374
  rb_raise(cMysql2Error, "%s", mysql_error(client));
241
375
  return Qnil;
242
376
  }
243
377
 
244
- result = mysql_store_result(client);
378
+ result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, client, RUBY_UBF_IO, 0);
245
379
  if (result == NULL) {
246
380
  if (mysql_field_count(client) != 0) {
247
381
  rb_raise(cMysql2Error, "%s", mysql_error(client));
@@ -280,15 +414,23 @@ static VALUE rb_mysql_result_to_obj(MYSQL_RES * r) {
280
414
  return obj;
281
415
  }
282
416
 
283
- void rb_mysql_result_free(void * wrapper) {
284
- mysql2_result_wrapper * w = wrapper;
285
- if (w && w->resultFreed != 1) {
286
- mysql_free_result(w->result);
287
- w->resultFreed = 1;
417
+ /* this may be called manually or during GC */
418
+ static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
419
+ if (wrapper && wrapper->resultFreed != 1) {
420
+ mysql_free_result(wrapper->result);
421
+ wrapper->resultFreed = 1;
288
422
  }
289
423
  }
290
424
 
291
- void rb_mysql_result_mark(void * wrapper) {
425
+ /* this is called during GC */
426
+ static void rb_mysql_result_free(void * wrapper) {
427
+ mysql2_result_wrapper * w = wrapper;
428
+ /* FIXME: this may call flush_use_result, which can hit the socket */
429
+ rb_mysql_result_free_result(w);
430
+ xfree(wrapper);
431
+ }
432
+
433
+ static void rb_mysql_result_mark(void * wrapper) {
292
434
  mysql2_result_wrapper * w = wrapper;
293
435
  if (w) {
294
436
  rb_gc_mark(w->fields);
@@ -296,14 +438,26 @@ void rb_mysql_result_mark(void * wrapper) {
296
438
  }
297
439
  }
298
440
 
441
+ /*
442
+ * for small results, this won't hit the network, but there's no
443
+ * reliable way for us to tell this so we'll always release the GVL
444
+ * to be safe
445
+ */
446
+ static VALUE nogvl_fetch_row(void *ptr)
447
+ {
448
+ MYSQL_RES *result = ptr;
449
+
450
+ return (VALUE)mysql_fetch_row(result);
451
+ }
452
+
299
453
  static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
300
454
  VALUE rowHash, opts, block;
301
455
  mysql2_result_wrapper * wrapper;
302
456
  MYSQL_ROW row;
303
457
  MYSQL_FIELD * fields = NULL;
304
- struct tm parsedTime;
305
458
  unsigned int i = 0, symbolizeKeys = 0;
306
459
  unsigned long * fieldLengths;
460
+ void * ptr;
307
461
 
308
462
  GetMysql2Result(self, wrapper);
309
463
 
@@ -314,7 +468,8 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
314
468
  }
315
469
  }
316
470
 
317
- row = mysql_fetch_row(wrapper->result);
471
+ ptr = wrapper->result;
472
+ row = (MYSQL_ROW)rb_thread_blocking_region(nogvl_fetch_row, ptr, RUBY_UBF_IO, 0);
318
473
  if (row == NULL) {
319
474
  return Qnil;
320
475
  }
@@ -353,8 +508,10 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
353
508
  case MYSQL_TYPE_NULL: // NULL-type field
354
509
  val = Qnil;
355
510
  break;
356
- case MYSQL_TYPE_TINY: // TINYINT field
357
511
  case MYSQL_TYPE_BIT: // BIT field (MySQL 5.0.3 and up)
512
+ val = rb_str_new(row[i], fieldLengths[i]);
513
+ break;
514
+ case MYSQL_TYPE_TINY: // TINYINT field
358
515
  case MYSQL_TYPE_SHORT: // SMALLINT field
359
516
  case MYSQL_TYPE_LONG: // INTEGER field
360
517
  case MYSQL_TYPE_INT24: // MEDIUMINT field
@@ -370,32 +527,42 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
370
527
  case MYSQL_TYPE_DOUBLE: // DOUBLE or REAL field
371
528
  val = rb_float_new(strtod(row[i], NULL));
372
529
  break;
373
- case MYSQL_TYPE_TIME: // TIME field
374
- if (memcmp("00:00:00", row[i], 8) == 0) {
375
- val = Qnil;
376
- } else {
377
- strptime(row[i], "%T", &parsedTime);
378
- val = rb_funcall(rb_cTime, intern_local, 6, INT2NUM(1900+parsedTime.tm_year), INT2NUM(parsedTime.tm_mon+1), INT2NUM(parsedTime.tm_mday), INT2NUM(parsedTime.tm_hour), INT2NUM(parsedTime.tm_min), INT2NUM(parsedTime.tm_sec));
379
- }
530
+ case MYSQL_TYPE_TIME: { // TIME field
531
+ int hour, min, sec, tokens;
532
+ tokens = sscanf(row[i], "%2d:%2d:%2d", &hour, &min, &sec);
533
+ val = rb_funcall(rb_cTime, intern_local, 6, INT2NUM(0), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec));
380
534
  break;
535
+ }
381
536
  case MYSQL_TYPE_TIMESTAMP: // TIMESTAMP field
382
- case MYSQL_TYPE_DATETIME: // DATETIME field
383
- if (memcmp("0000-00-00 00:00:00", row[i], 19) == 0) {
537
+ case MYSQL_TYPE_DATETIME: { // DATETIME field
538
+ int year, month, day, hour, min, sec, tokens;
539
+ tokens = sscanf(row[i], "%4d-%2d-%2d %2d:%2d:%2d", &year, &month, &day, &hour, &min, &sec);
540
+ if (year+month+day+hour+min+sec == 0) {
384
541
  val = Qnil;
385
542
  } else {
386
- strptime(row[i], "%F %T", &parsedTime);
387
- val = rb_funcall(rb_cTime, intern_local, 6, INT2NUM(1900+parsedTime.tm_year), INT2NUM(parsedTime.tm_mon+1), INT2NUM(parsedTime.tm_mday), INT2NUM(parsedTime.tm_hour), INT2NUM(parsedTime.tm_min), INT2NUM(parsedTime.tm_sec));
543
+ if (month < 1 || day < 1) {
544
+ rb_raise(cMysql2Error, "Invalid date: %s", row[i]);
545
+ } else {
546
+ val = rb_funcall(rb_cTime, intern_local, 6, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec));
547
+ }
388
548
  }
389
549
  break;
550
+ }
390
551
  case MYSQL_TYPE_DATE: // DATE field
391
- case MYSQL_TYPE_NEWDATE: // Newer const used > 5.0
392
- if (memcmp("0000-00-00", row[i], 10) == 0) {
552
+ case MYSQL_TYPE_NEWDATE: { // Newer const used > 5.0
553
+ int year, month, day, tokens;
554
+ tokens = sscanf(row[i], "%4d-%2d-%2d", &year, &month, &day);
555
+ if (year+month+day == 0) {
393
556
  val = Qnil;
394
557
  } else {
395
- strptime(row[i], "%F", &parsedTime);
396
- val = rb_funcall(rb_cTime, intern_local, 3, INT2NUM(1900+parsedTime.tm_year), INT2NUM(parsedTime.tm_mon+1), INT2NUM(parsedTime.tm_mday));
558
+ if (month < 1 || day < 1) {
559
+ rb_raise(cMysql2Error, "Invalid date: %s", row[i]);
560
+ } else {
561
+ val = rb_funcall(cDate, intern_new, 3, INT2NUM(year), INT2NUM(month), INT2NUM(day));
562
+ }
397
563
  }
398
564
  break;
565
+ }
399
566
  case MYSQL_TYPE_TINY_BLOB:
400
567
  case MYSQL_TYPE_MEDIUM_BLOB:
401
568
  case MYSQL_TYPE_LONG_BLOB:
@@ -464,7 +631,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
464
631
 
465
632
  if (row == Qnil) {
466
633
  // we don't need the mysql C dataset around anymore, peace it
467
- rb_mysql_result_free(wrapper->result);
634
+ rb_mysql_result_free_result(wrapper);
468
635
  return Qnil;
469
636
  }
470
637
 
@@ -474,7 +641,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
474
641
  }
475
642
  if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
476
643
  // we don't need the mysql C dataset around anymore, peace it
477
- rb_mysql_result_free(wrapper);
644
+ rb_mysql_result_free_result(wrapper);
478
645
  }
479
646
  }
480
647
 
@@ -495,6 +662,7 @@ void Init_mysql2_ext() {
495
662
  VALUE cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject);
496
663
  rb_define_singleton_method(cMysql2Client, "new", rb_mysql_client_new, -1);
497
664
  rb_define_method(cMysql2Client, "initialize", rb_mysql_client_init, -1);
665
+ rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0);
498
666
  rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1);
499
667
  rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1);
500
668
  rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0);
@@ -537,4 +705,4 @@ void Init_mysql2_ext() {
537
705
  utf8Encoding = rb_enc_find_index("UTF-8");
538
706
  binaryEncoding = rb_enc_find_index("binary");
539
707
  #endif
540
- }
708
+ }