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.
- data/CHANGELOG.md +19 -0
- data/README.rdoc +4 -4
- data/VERSION +1 -1
- data/benchmark/active_record.rb +17 -5
- data/benchmark/query_with_mysql_casting.rb +82 -0
- data/benchmark/{query.rb → query_without_mysql_casting.rb} +0 -0
- data/benchmark/sequel.rb +39 -0
- data/ext/extconf.rb +3 -0
- data/ext/mysql2_ext.c +234 -66
- data/ext/mysql2_ext.h +69 -9
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +25 -8
- data/lib/mysql2.rb +1 -1
- data/lib/sequel/adapters/mysql2.rb +238 -0
- data/mysql2.gemspec +6 -3
- data/spec/active_record/active_record_spec.rb +124 -1
- data/spec/mysql2/client_spec.rb +15 -0
- data/spec/mysql2/result_spec.rb +89 -42
- metadata +7 -4
data/CHANGELOG.md
CHANGED
@@ -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
|
data/README.rdoc
CHANGED
@@ -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
|
-
|
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.
|
136
|
+
0.890000 0.190000 1.080000 ( 2.028887)
|
137
137
|
Mysql
|
138
|
-
|
138
|
+
7.330000 0.350000 7.680000 ( 8.013160)
|
139
139
|
do_mysql
|
140
|
-
1.
|
140
|
+
1.740000 0.220000 1.960000 ( 2.909290)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.5
|
data/benchmark/active_record.rb
CHANGED
@@ -15,24 +15,36 @@ mysql_opts = {
|
|
15
15
|
:database => 'test'
|
16
16
|
}
|
17
17
|
|
18
|
-
class
|
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
|
-
|
28
|
+
Mysql2Model.establish_connection(mysql2_opts)
|
25
29
|
puts "Mysql2"
|
26
30
|
number_of.times do
|
27
|
-
|
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
|
-
|
40
|
+
MysqlModel.establish_connection(mysql_opts)
|
33
41
|
puts "Mysql"
|
34
42
|
number_of.times do
|
35
|
-
|
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
|
File without changes
|
data/benchmark/sequel.rb
ADDED
@@ -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
|
data/ext/extconf.rb
CHANGED
data/ext/mysql2_ext.c
CHANGED
@@ -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
|
-
|
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
|
67
|
+
unsigned int connect_timeout = 0;
|
17
68
|
my_bool reconnect = 1;
|
18
69
|
|
19
|
-
obj = Data_Make_Struct(klass,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 (
|
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(
|
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(
|
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(
|
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(
|
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(
|
156
|
+
rb_warn("%s\n", mysql_error(args.mysql));
|
106
157
|
}
|
107
158
|
|
108
159
|
// force the encoding to utf8
|
109
|
-
if (mysql_options(
|
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(
|
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(
|
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 (
|
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(
|
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 *
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
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
|
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,
|
157
|
-
if (
|
158
|
-
rb_raise(cMysql2Error, "%s", mysql_error(
|
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 =
|
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 (
|
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 =
|
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
|
-
|
284
|
-
|
285
|
-
if (
|
286
|
-
mysql_free_result(
|
287
|
-
|
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
|
-
|
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
|
-
|
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:
|
374
|
-
|
375
|
-
|
376
|
-
|
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:
|
383
|
-
|
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
|
-
|
387
|
-
|
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:
|
392
|
-
|
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
|
-
|
396
|
-
|
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
|
-
|
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
|
-
|
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
|
+
}
|