mysql2 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }