mysql2 0.1.9 → 0.2.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.
data/.gitignore CHANGED
@@ -4,5 +4,8 @@ Makefile
4
4
  *.bundle
5
5
  *.so
6
6
  *.a
7
+ *.rbc
7
8
  mkmf.log
8
9
  pkg/
10
+ tmp
11
+ vendor
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.9 (HEAD)
3
+ ## 0.2.0 (August 16th, 2010)
4
+ * switch back to letting libmysql manage all allocation/thread-state/freeing for the connection
5
+ * cache various numeric type conversions in hot-spots of the code for a little speed boost
6
+ * ActiveRecord adapter moved into Rails 3 core
7
+ ** Don't worry 2.3.x users! We'll either release the adapter as a separate gem, or try to get it into 2.3.9
8
+ * Fix for the "closed MySQL connection" error (GH #34)
9
+ * Fix for the "can't modify frozen object" error in 1.9.2 (GH #37)
10
+ * Introduce cascading query and result options (more info in README)
11
+ * Sequel adapter pulled into core (will be in the next release - 3.15.0 at the time of writing)
12
+ * add a safety check when attempting to send a query before a result has been fetched
13
+
14
+ ## 0.1.9 (July 17th, 2010)
4
15
  * Support async ActiveRecord access with fibers and EventMachine (mperham)
5
16
  * string encoding support for 1.9, respecting Encoding.default_internal
6
17
  * added support for rake-compiler (tenderlove)
@@ -58,6 +58,67 @@ How about with symbolized keys?
58
58
  # do something with row, it's ready to rock
59
59
  end
60
60
 
61
+ == Cascading config
62
+
63
+ The default config hash is at:
64
+
65
+ Mysql2::Client.default_query_options
66
+
67
+ which defaults to:
68
+
69
+ {:async => false, :as => :hash, :symbolize_keys => false}
70
+
71
+ that can be used as so:
72
+
73
+ # these are the defaults all Mysql2::Client instances inherit
74
+ Mysql2::Client.default_query_options.merge!(:as => :array)
75
+
76
+ or
77
+
78
+ # this will change the defaults for all future results returned by the #query method _for this connection only_
79
+ c = Mysql2::Client.new
80
+ c.query_options.merge!(:symbolize_keys => true)
81
+
82
+ or
83
+
84
+ # this will set the options for the Mysql2::Result instance returned from the #query method
85
+ c = Mysql2::Client.new
86
+ c.query(sql, :symbolize_keys => true)
87
+
88
+ == Result types
89
+
90
+ === Array of Arrays
91
+
92
+ Pass the {:as => :array} option to any of the above methods of configuration
93
+
94
+ === Array of Hashes
95
+
96
+ The default result type is set to :hash, but you can override a previous setting to something else with {:as => :hash}
97
+
98
+ === Others...
99
+
100
+ I may add support for {:as => :csv} or even {:as => :json} to allow for *much* more efficient generation of those data types from result sets.
101
+ If you'd like to see either of these (or others), open an issue and start bugging me about it ;)
102
+
103
+ == Timezones
104
+
105
+ Mysql2 now supports two timezone options:
106
+
107
+ :database_timezone - this is the timezone Mysql2 will assume fields are already stored as, and will use this when creating the initial Time objects in ruby
108
+ :application_timezone - this is the timezone Mysql2 will convert to before finally handing back to the caller
109
+
110
+ In other words, if :database_timezone is set to :utc - Mysql2 will create the Time objects using Time.utc(...) from the raw value libmysql hands over initially.
111
+ Then, if :application_timezone is set to say - :local - Mysql2 will then convert the just-created UTC Time object to local time.
112
+
113
+ Both options only allow two values - :local or :utc - with the exception that :application_timezone can be [and defaults to] nil
114
+
115
+ == Casting "boolean" columns
116
+
117
+ You can now tell Mysql2 to cast tinyint(1) fields to boolean values in Ruby with the :cast_booleans option.
118
+
119
+ client = Mysql2::Client.new
120
+ result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true)
121
+
61
122
  == Async
62
123
 
63
124
  Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries.
@@ -77,14 +138,17 @@ If you need multiple query concurrency take a look at using a connection pool.
77
138
 
78
139
  == ActiveRecord
79
140
 
80
- To use the ActiveRecord driver, all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2".
81
- That was easy right? :)
141
+ The ActiveRecord adapter has been removed from the mysql2 gem and is now part of Rails 3 core. We'll be releasing a separate gem or trying to get it into 2.3.9 for 2.3.x users.
82
142
 
83
143
  == Asynchronous ActiveRecord
84
144
 
85
145
  You can also use Mysql2 with asynchronous Rails (first introduced at http://www.mikeperham.com/2010/04/03/introducing-phat-an-asynchronous-rails-app/) by
86
146
  setting the adapter in your database.yml to "em_mysql2". You must be running Ruby 1.9, thin and the rack-fiber_pool middleware for it to work.
87
147
 
148
+ == Sequel
149
+
150
+ The Sequel adapter was pulled out into Sequel core (will be part of the next release) and can be used by specifying the "mysql2://" prefix to your connection specification.
151
+
88
152
  == EventMachine
89
153
 
90
154
  The mysql2 EventMachine deferrable api allows you to make async queries using EventMachine,
@@ -151,11 +215,11 @@ then iterating over every row using an #each like method yielding a block:
151
215
  # These results are from the query_with_mysql_casting.rb script in the benchmarks folder
152
216
  user system total real
153
217
  Mysql2
154
- 0.890000 0.190000 1.080000 ( 2.028887)
218
+ 0.750000 0.180000 0.930000 ( 1.821655)
155
219
  do_mysql
156
- 1.740000 0.220000 1.960000 ( 2.909290)
220
+ 1.650000 0.200000 1.850000 ( 2.811357)
157
221
  Mysql
158
- 7.330000 0.350000 7.680000 ( 8.013160)
222
+ 7.500000 0.210000 7.710000 ( 8.065871)
159
223
 
160
224
  == Special Thanks
161
225
 
data/Rakefile CHANGED
@@ -20,8 +20,6 @@ end
20
20
 
21
21
  require 'rake'
22
22
  require 'spec/rake/spectask'
23
- gem 'rake-compiler', '>= 0.4.1'
24
- require "rake/extensiontask"
25
23
 
26
24
  desc "Run all examples with RCov"
27
25
  Spec::Rake::SpecTask.new('spec:rcov') do |t|
@@ -36,7 +34,7 @@ Spec::Rake::SpecTask.new('spec') do |t|
36
34
  t.spec_opts << '--options' << 'spec/spec.opts'
37
35
  end
38
36
 
39
- Rake::ExtensionTask.new("mysql2", JEWELER.gemspec) do |ext|
40
- ext.lib_dir = File.join 'lib', 'mysql2'
41
- end
42
- Rake::Task[:spec].prerequisites << :compile
37
+ task :default => :spec
38
+
39
+ # Load custom tasks
40
+ Dir['tasks/*.rake'].sort.each { |f| load f }
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.9
1
+ 0.2.0
@@ -5,6 +5,9 @@ require 'rubygems'
5
5
  require 'benchmark'
6
6
  require 'active_record'
7
7
 
8
+ ActiveRecord::Base.default_timezone = :local
9
+ ActiveRecord::Base.time_zone_aware_attributes = true
10
+
8
11
  number_of = 10
9
12
  mysql2_opts = {
10
13
  :adapter => 'mysql2',
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ raise Mysql2::Mysql2Error.new("GC allocation benchmarks only supported on Ruby 1.9!") unless RUBY_VERSION =~ /1\.9/
5
+
6
+ require 'rubygems'
7
+ require 'benchmark'
8
+ require 'active_record'
9
+
10
+ ActiveRecord::Base.default_timezone = :local
11
+ ActiveRecord::Base.time_zone_aware_attributes = true
12
+
13
+ class Mysql2Model < ActiveRecord::Base
14
+ set_table_name :mysql2_test
15
+ end
16
+
17
+ def bench_allocations(feature, iterations = 10, &blk)
18
+ puts "GC overhead for #{feature}"
19
+ Mysql2Model.establish_connection(:adapter => 'mysql2', :database => 'test')
20
+ GC::Profiler.clear
21
+ GC::Profiler.enable
22
+ iterations.times{ blk.call }
23
+ GC::Profiler.report(STDOUT)
24
+ GC::Profiler.disable
25
+ end
26
+
27
+ bench_allocations('coercion') do
28
+ Mysql2Model.all(:limit => 1000).each{ |r|
29
+ r.attributes.keys.each{ |k|
30
+ r.send(k.to_sym)
31
+ }
32
+ }
33
+ end
@@ -7,31 +7,33 @@ require 'mysql'
7
7
  require 'mysql2'
8
8
  require 'do_mysql'
9
9
 
10
- number_of = 1000
11
- str = "abc'def\"ghi\0jkl%mno"
12
-
13
- Benchmark.bmbm do |x|
14
- mysql = Mysql.new("localhost", "root")
15
- x.report do
16
- puts "Mysql"
17
- number_of.times do
18
- mysql.quote str
10
+ def run_escape_benchmarks(str, number_of = 1000)
11
+ Benchmark.bmbm do |x|
12
+ mysql = Mysql.new("localhost", "root")
13
+ x.report do
14
+ puts "Mysql #{str.inspect}"
15
+ number_of.times do
16
+ mysql.quote str
17
+ end
19
18
  end
20
- end
21
19
 
22
- mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root")
23
- x.report do
24
- puts "Mysql2"
25
- number_of.times do
26
- mysql2.escape str
20
+ mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root")
21
+ x.report do
22
+ puts "Mysql2 #{str.inspect}"
23
+ number_of.times do
24
+ mysql2.escape str
25
+ end
27
26
  end
28
- end
29
27
 
30
- do_mysql = DataObjects::Connection.new("mysql://localhost/test")
31
- x.report do
32
- puts "do_mysql"
33
- number_of.times do
34
- do_mysql.quote_string str
28
+ do_mysql = DataObjects::Connection.new("mysql://localhost/test")
29
+ x.report do
30
+ puts "do_mysql #{str.inspect}"
31
+ number_of.times do
32
+ do_mysql.quote_string str
33
+ end
35
34
  end
36
35
  end
37
- end
36
+ end
37
+
38
+ run_escape_benchmarks "abc'def\"ghi\0jkl%mno"
39
+ run_escape_benchmarks "clean string"
@@ -45,8 +45,8 @@ Benchmark.bmbm do |x|
45
45
  x.report do
46
46
  puts "Mysql2"
47
47
  number_of.times do
48
- mysql2_result = mysql2.query sql
49
- mysql2_result.each(:symbolize_keys => true) do |res|
48
+ mysql2_result = mysql2.query sql, :symbolize_keys => true
49
+ mysql2_result.each do |res|
50
50
  # puts res.inspect
51
51
  end
52
52
  end
@@ -17,8 +17,8 @@ Benchmark.bmbm do |x|
17
17
  x.report do
18
18
  puts "Mysql2"
19
19
  number_of.times do
20
- mysql2_result = mysql2.query sql
21
- mysql2_result.each(:symbolize_keys => true) do |res|
20
+ mysql2_result = mysql2.query sql, :symbolize_keys => true
21
+ mysql2_result.each do |res|
22
22
  # puts res.inspect
23
23
  end
24
24
  end
@@ -22,8 +22,10 @@ create_table_sql = %[
22
22
  int_test INT,
23
23
  big_int_test BIGINT,
24
24
  float_test FLOAT(10,3),
25
+ float_zero_test FLOAT(10,3),
25
26
  double_test DOUBLE(10,3),
26
27
  decimal_test DECIMAL(10,3),
28
+ decimal_zero_test DECIMAL(10,3),
27
29
  date_test DATE,
28
30
  date_time_test DATETIME,
29
31
  timestamp_test TIMESTAMP,
@@ -55,7 +57,7 @@ def insert_record(args)
55
57
  insert_sql = "
56
58
  INSERT INTO mysql2_test (
57
59
  null_test, bit_test, tiny_int_test, small_int_test, medium_int_test, int_test, big_int_test,
58
- float_test, double_test, decimal_test, date_test, date_time_test, timestamp_test, time_test,
60
+ float_test, float_zero_test, double_test, decimal_test, decimal_zero_test, date_test, date_time_test, timestamp_test, time_test,
59
61
  year_test, char_test, varchar_test, binary_test, varbinary_test, tiny_blob_test,
60
62
  tiny_text_test, blob_test, text_test, medium_blob_test, medium_text_test,
61
63
  long_blob_test, long_text_test, enum_test, set_test
@@ -63,7 +65,7 @@ def insert_record(args)
63
65
 
64
66
  VALUES (
65
67
  NULL, #{args[:bit_test]}, #{args[:tiny_int_test]}, #{args[:small_int_test]}, #{args[:medium_int_test]}, #{args[:int_test]}, #{args[:big_int_test]},
66
- #{args[:float_test]}, #{args[:double_test]}, #{args[:decimal_test]}, '#{args[:date_test]}', '#{args[:date_time_test]}', '#{args[:timestamp_test]}', '#{args[:time_test]}',
68
+ #{args[:float_test]}, #{args[:float_zero_test]}, #{args[:double_test]}, #{args[:decimal_test]}, #{args[:decimal_zero_test]}, '#{args[:date_test]}', '#{args[:date_time_test]}', '#{args[:timestamp_test]}', '#{args[:time_test]}',
67
69
  #{args[:year_test]}, '#{args[:char_test]}', '#{args[:varchar_test]}', '#{args[:binary_test]}', '#{args[:varbinary_test]}', '#{args[:tiny_blob_test]}',
68
70
  '#{args[:tiny_text_test]}', '#{args[:blob_test]}', '#{args[:text_test]}', '#{args[:medium_blob_test]}', '#{args[:medium_text_test]}',
69
71
  '#{args[:long_blob_test]}', '#{args[:long_text_test]}', '#{args[:enum_test]}', '#{args[:set_test]}'
@@ -82,8 +84,10 @@ num.times do |n|
82
84
  :int_test => rand(2147483647),
83
85
  :big_int_test => rand(9223372036854775807),
84
86
  :float_test => rand(32767)/1.87,
87
+ :float_zero_test => 0.0,
85
88
  :double_test => rand(8388607)/1.87,
86
89
  :decimal_test => rand(8388607)/1.87,
90
+ :decimal_zero_test => 0,
87
91
  :date_test => '2010-4-4',
88
92
  :date_time_test => '2010-4-4 11:44:00',
89
93
  :timestamp_test => '2010-4-4 11:44:00',
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'benchmark'
6
+ require 'mysql2'
7
+
8
+ iterations = 1000
9
+ client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => "test")
10
+ query = lambda{ iterations.times{ client.query("SELECT mysql2_test.* FROM mysql2_test") } }
11
+ Benchmark.bmbm do |x|
12
+ x.report('select') do
13
+ query.call
14
+ end
15
+ x.report('rb_thread_select') do
16
+ thread = Thread.new{ sleep(10) }
17
+ query.call
18
+ thread.kill
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+ require 'mysql2'
5
+ require 'timeout'
6
+
7
+ threads = []
8
+ # Should never exceed worst case 3.5 secs across all 20 threads
9
+ Timeout.timeout(3.5) do
10
+ 20.times do
11
+ threads << Thread.new do
12
+ overhead = rand(3)
13
+ puts ">> thread #{Thread.current.object_id} query, #{overhead} sec overhead"
14
+ # 3 second overhead per query
15
+ Mysql2::Client.new(:host => "localhost", :username => "root").query("SELECT sleep(#{overhead}) as result")
16
+ puts "<< thread #{Thread.current.object_id} result, #{overhead} sec overhead"
17
+ end
18
+ end
19
+ threads.each{|t| t.join }
20
+ end
@@ -0,0 +1,540 @@
1
+ #include <mysql2_ext.h>
2
+ #include <client.h>
3
+
4
+ VALUE cMysql2Client;
5
+ extern VALUE mMysql2, cMysql2Error;
6
+ static VALUE intern_encoding_from_charset;
7
+ static ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array;
8
+ static ID intern_merge, intern_error_number_eql, intern_sql_state_eql;
9
+
10
+ #define REQUIRE_OPEN_DB(_ctxt) \
11
+ if(!_ctxt->net.vio) { \
12
+ rb_raise(cMysql2Error, "closed MySQL connection"); \
13
+ return Qnil; \
14
+ }
15
+
16
+ #define MARK_CONN_INACTIVE(conn) \
17
+ wrapper->active = 0;
18
+
19
+ #define GET_CLIENT(self) \
20
+ mysql_client_wrapper *wrapper; \
21
+ MYSQL *client; \
22
+ Data_Get_Struct(self, mysql_client_wrapper, wrapper); \
23
+ client = &wrapper->client;
24
+
25
+ /*
26
+ * used to pass all arguments to mysql_real_connect while inside
27
+ * rb_thread_blocking_region
28
+ */
29
+ struct nogvl_connect_args {
30
+ MYSQL *mysql;
31
+ const char *host;
32
+ const char *user;
33
+ const char *passwd;
34
+ const char *db;
35
+ unsigned int port;
36
+ const char *unix_socket;
37
+ unsigned long client_flag;
38
+ };
39
+
40
+ /*
41
+ * used to pass all arguments to mysql_send_query while inside
42
+ * rb_thread_blocking_region
43
+ */
44
+ struct nogvl_send_query_args {
45
+ MYSQL *mysql;
46
+ VALUE sql;
47
+ };
48
+
49
+ /*
50
+ * non-blocking mysql_*() functions that we won't be wrapping since
51
+ * they do not appear to hit the network nor issue any interruptible
52
+ * or blocking system calls.
53
+ *
54
+ * - mysql_affected_rows()
55
+ * - mysql_error()
56
+ * - mysql_fetch_fields()
57
+ * - mysql_fetch_lengths() - calls cli_fetch_lengths or emb_fetch_lengths
58
+ * - mysql_field_count()
59
+ * - mysql_get_client_info()
60
+ * - mysql_get_client_version()
61
+ * - mysql_get_server_info()
62
+ * - mysql_get_server_version()
63
+ * - mysql_insert_id()
64
+ * - mysql_num_fields()
65
+ * - mysql_num_rows()
66
+ * - mysql_options()
67
+ * - mysql_real_escape_string()
68
+ * - mysql_ssl_set()
69
+ */
70
+
71
+ static void rb_mysql_client_mark(void * wrapper) {
72
+ mysql_client_wrapper * w = wrapper;
73
+ if (w) {
74
+ rb_gc_mark(w->encoding);
75
+ }
76
+ }
77
+
78
+ static VALUE rb_raise_mysql2_error(MYSQL *client) {
79
+ VALUE e = rb_exc_new2(cMysql2Error, mysql_error(client));
80
+ rb_funcall(e, intern_error_number_eql, 1, INT2NUM(mysql_errno(client)));
81
+ rb_funcall(e, intern_sql_state_eql, 1, rb_tainted_str_new2(mysql_sqlstate(client)));
82
+ rb_exc_raise(e);
83
+ return Qnil;
84
+ }
85
+
86
+ static VALUE nogvl_init(void *ptr) {
87
+ MYSQL * client = (MYSQL *)ptr;
88
+
89
+ /* may initialize embedded server and read /etc/services off disk */
90
+ client = mysql_init(NULL);
91
+
92
+ return client ? Qtrue : Qfalse;
93
+ }
94
+
95
+ static VALUE nogvl_connect(void *ptr) {
96
+ struct nogvl_connect_args *args = ptr;
97
+ MYSQL *client;
98
+
99
+ client = mysql_real_connect(args->mysql, args->host,
100
+ args->user, args->passwd,
101
+ args->db, args->port, args->unix_socket,
102
+ args->client_flag);
103
+
104
+ return client ? Qtrue : Qfalse;
105
+ }
106
+
107
+ static void rb_mysql_client_free(void * ptr) {
108
+ mysql_client_wrapper * wrapper = (mysql_client_wrapper *)ptr;
109
+ MYSQL * client = &wrapper->client;
110
+
111
+ /*
112
+ * we'll send a QUIT message to the server, but that message is more of a
113
+ * formality than a hard requirement since the socket is getting shutdown
114
+ * anyways, so ensure the socket write does not block our interpreter
115
+ */
116
+ int fd = client->net.fd;
117
+ int flags;
118
+
119
+ if (fd >= 0) {
120
+ /*
121
+ * if the socket is dead we have no chance of blocking,
122
+ * so ignore any potential fcntl errors since they don't matter
123
+ */
124
+ flags = fcntl(fd, F_GETFL);
125
+ if (flags > 0 && !(flags & O_NONBLOCK))
126
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
127
+ }
128
+
129
+ /* It's safe to call mysql_close() on an already closed connection. */
130
+ mysql_close(client);
131
+ xfree(ptr);
132
+ }
133
+
134
+ static VALUE nogvl_close(void * ptr) {
135
+ MYSQL *client = (MYSQL *)ptr;
136
+ mysql_close(client);
137
+ client->net.fd = -1;
138
+ return Qnil;
139
+ }
140
+
141
+ static VALUE allocate(VALUE klass) {
142
+ VALUE obj;
143
+ mysql_client_wrapper * wrapper;
144
+ obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper);
145
+ wrapper->encoding = Qnil;
146
+ wrapper->active = 0;
147
+ return obj;
148
+ }
149
+
150
+ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket) {
151
+ struct nogvl_connect_args args;
152
+ GET_CLIENT(self)
153
+
154
+ args.host = NIL_P(host) ? "localhost" : StringValuePtr(host);
155
+ args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket);
156
+ args.port = NIL_P(port) ? 3306 : NUM2INT(port);
157
+ args.user = NIL_P(user) ? NULL : StringValuePtr(user);
158
+ args.passwd = NIL_P(pass) ? NULL : StringValuePtr(pass);
159
+ args.db = NIL_P(database) ? NULL : StringValuePtr(database);
160
+ args.mysql = client;
161
+ args.client_flag = 0;
162
+
163
+ if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) {
164
+ // unable to connect
165
+ return rb_raise_mysql2_error(client);
166
+ }
167
+
168
+ return self;
169
+ }
170
+
171
+ /*
172
+ * Immediately disconnect from the server, normally the garbage collector
173
+ * will disconnect automatically when a connection is no longer needed.
174
+ * Explicitly closing this will free up server resources sooner than waiting
175
+ * for the garbage collector.
176
+ */
177
+ static VALUE rb_mysql_client_close(VALUE self) {
178
+ GET_CLIENT(self)
179
+
180
+ rb_thread_blocking_region(nogvl_close, client, RUBY_UBF_IO, 0);
181
+
182
+ return Qnil;
183
+ }
184
+
185
+ /*
186
+ * mysql_send_query is unlikely to block since most queries are small
187
+ * enough to fit in a socket buffer, but sometimes large UPDATE and
188
+ * INSERTs will cause the process to block
189
+ */
190
+ static VALUE nogvl_send_query(void *ptr) {
191
+ struct nogvl_send_query_args *args = ptr;
192
+ int rv;
193
+ const char *sql = StringValuePtr(args->sql);
194
+ long sql_len = RSTRING_LEN(args->sql);
195
+
196
+ rv = mysql_send_query(args->mysql, sql, sql_len);
197
+
198
+ return rv == 0 ? Qtrue : Qfalse;
199
+ }
200
+
201
+ /*
202
+ * even though we did rb_thread_select before calling this, a large
203
+ * response can overflow the socket buffers and cause us to eventually
204
+ * block while calling mysql_read_query_result
205
+ */
206
+ static VALUE nogvl_read_query_result(void *ptr) {
207
+ MYSQL * client = ptr;
208
+ my_bool res = mysql_read_query_result(client);
209
+
210
+ return res == 0 ? Qtrue : Qfalse;
211
+ }
212
+
213
+ /* mysql_store_result may (unlikely) read rows off the socket */
214
+ static VALUE nogvl_store_result(void *ptr) {
215
+ MYSQL * client = ptr;
216
+ return (VALUE)mysql_store_result(client);
217
+ }
218
+
219
+ static VALUE rb_mysql_client_async_result(VALUE self) {
220
+ MYSQL_RES * result;
221
+ GET_CLIENT(self)
222
+
223
+ REQUIRE_OPEN_DB(client);
224
+ if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) {
225
+ // an error occurred, mark this connection inactive
226
+ MARK_CONN_INACTIVE(self);
227
+ return rb_raise_mysql2_error(client);
228
+ }
229
+
230
+ result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, client, RUBY_UBF_IO, 0);
231
+
232
+ // we have our result, mark this connection inactive
233
+ MARK_CONN_INACTIVE(self);
234
+
235
+ if (result == NULL) {
236
+ if (mysql_field_count(client) != 0) {
237
+ rb_raise_mysql2_error(client);
238
+ }
239
+ return Qnil;
240
+ }
241
+
242
+ VALUE resultObj = rb_mysql_result_to_obj(result);
243
+ // pass-through query options for result construction later
244
+ rb_iv_set(resultObj, "@query_options", rb_obj_dup(rb_iv_get(self, "@query_options")));
245
+
246
+ #ifdef HAVE_RUBY_ENCODING_H
247
+ mysql2_result_wrapper * result_wrapper;
248
+ GetMysql2Result(resultObj, result_wrapper);
249
+ result_wrapper->encoding = wrapper->encoding;
250
+ #endif
251
+ return resultObj;
252
+ }
253
+
254
+ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
255
+ struct nogvl_send_query_args args;
256
+ fd_set fdset;
257
+ int fd, retval;
258
+ int async = 0;
259
+ VALUE opts, defaults;
260
+ int(*selector)(int, fd_set *, fd_set *, fd_set *, struct timeval *) = NULL;
261
+ GET_CLIENT(self)
262
+
263
+ REQUIRE_OPEN_DB(client);
264
+ args.mysql = client;
265
+
266
+ // see if this connection is still waiting on a result from a previous query
267
+ if (wrapper->active == 0) {
268
+ // mark this connection active
269
+ wrapper->active = 1;
270
+ } else {
271
+ rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result");
272
+ }
273
+
274
+ defaults = rb_iv_get(self, "@query_options");
275
+ if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) {
276
+ opts = rb_funcall(defaults, intern_merge, 1, opts);
277
+ rb_iv_set(self, "@query_options", opts);
278
+
279
+ if (rb_hash_aref(opts, sym_async) == Qtrue) {
280
+ async = 1;
281
+ }
282
+ } else {
283
+ opts = defaults;
284
+ }
285
+
286
+ #ifdef HAVE_RUBY_ENCODING_H
287
+ rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding);
288
+ // ensure the string is in the encoding the connection is expecting
289
+ args.sql = rb_str_export_to_enc(args.sql, conn_enc);
290
+ #endif
291
+
292
+ if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) {
293
+ // an error occurred, we're not active anymore
294
+ MARK_CONN_INACTIVE(self);
295
+ return rb_raise_mysql2_error(client);
296
+ }
297
+
298
+ if (!async) {
299
+ // the below code is largely from do_mysql
300
+ // http://github.com/datamapper/do
301
+ fd = client->net.fd;
302
+ selector = rb_thread_alone() ? *select : *rb_thread_select;
303
+ for(;;) {
304
+ FD_ZERO(&fdset);
305
+ FD_SET(fd, &fdset);
306
+
307
+ retval = selector(fd + 1, &fdset, NULL, NULL, NULL);
308
+
309
+ if (retval < 0) {
310
+ rb_sys_fail(0);
311
+ }
312
+
313
+ if (retval > 0) {
314
+ break;
315
+ }
316
+ }
317
+
318
+ VALUE result = rb_mysql_client_async_result(self);
319
+
320
+ return result;
321
+ } else {
322
+ return Qnil;
323
+ }
324
+ }
325
+
326
+ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) {
327
+ VALUE newStr;
328
+ unsigned long newLen, oldLen;
329
+ GET_CLIENT(self)
330
+
331
+ Check_Type(str, T_STRING);
332
+ #ifdef HAVE_RUBY_ENCODING_H
333
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
334
+ rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding);
335
+ // ensure the string is in the encoding the connection is expecting
336
+ str = rb_str_export_to_enc(str, conn_enc);
337
+ #endif
338
+
339
+ oldLen = RSTRING_LEN(str);
340
+ char escaped[(oldLen*2)+1];
341
+
342
+ REQUIRE_OPEN_DB(client);
343
+ newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), oldLen);
344
+ if (newLen == oldLen) {
345
+ // no need to return a new ruby string if nothing changed
346
+ return str;
347
+ } else {
348
+ newStr = rb_str_new(escaped, newLen);
349
+ #ifdef HAVE_RUBY_ENCODING_H
350
+ rb_enc_associate(newStr, conn_enc);
351
+ if (default_internal_enc) {
352
+ newStr = rb_str_export_to_enc(newStr, default_internal_enc);
353
+ }
354
+ #endif
355
+ return newStr;
356
+ }
357
+ }
358
+
359
+ static VALUE rb_mysql_client_info(VALUE self) {
360
+ VALUE version = rb_hash_new(), client_info;
361
+ GET_CLIENT(self);
362
+ #ifdef HAVE_RUBY_ENCODING_H
363
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
364
+ rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding);
365
+ #endif
366
+
367
+ rb_hash_aset(version, sym_id, LONG2NUM(mysql_get_client_version()));
368
+ client_info = rb_str_new2(mysql_get_client_info());
369
+ #ifdef HAVE_RUBY_ENCODING_H
370
+ rb_enc_associate(client_info, conn_enc);
371
+ if (default_internal_enc) {
372
+ client_info = rb_str_export_to_enc(client_info, default_internal_enc);
373
+ }
374
+ #endif
375
+ rb_hash_aset(version, sym_version, client_info);
376
+ return version;
377
+ }
378
+
379
+ static VALUE rb_mysql_client_server_info(VALUE self) {
380
+ VALUE version, server_info;
381
+ GET_CLIENT(self)
382
+ #ifdef HAVE_RUBY_ENCODING_H
383
+ rb_encoding *default_internal_enc = rb_default_internal_encoding();
384
+ rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding);
385
+ #endif
386
+
387
+ REQUIRE_OPEN_DB(client);
388
+
389
+ version = rb_hash_new();
390
+ rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client)));
391
+ server_info = rb_str_new2(mysql_get_server_info(client));
392
+ #ifdef HAVE_RUBY_ENCODING_H
393
+ rb_enc_associate(server_info, conn_enc);
394
+ if (default_internal_enc) {
395
+ server_info = rb_str_export_to_enc(server_info, default_internal_enc);
396
+ }
397
+ #endif
398
+ rb_hash_aset(version, sym_version, server_info);
399
+ return version;
400
+ }
401
+
402
+ static VALUE rb_mysql_client_socket(VALUE self) {
403
+ GET_CLIENT(self)
404
+ REQUIRE_OPEN_DB(client);
405
+ return INT2NUM(client->net.fd);
406
+ }
407
+
408
+ static VALUE rb_mysql_client_last_id(VALUE self) {
409
+ GET_CLIENT(self)
410
+ REQUIRE_OPEN_DB(client);
411
+ return ULL2NUM(mysql_insert_id(client));
412
+ }
413
+
414
+ static VALUE rb_mysql_client_affected_rows(VALUE self) {
415
+ GET_CLIENT(self)
416
+ REQUIRE_OPEN_DB(client);
417
+ return ULL2NUM(mysql_affected_rows(client));
418
+ }
419
+
420
+ static VALUE set_reconnect(VALUE self, VALUE value) {
421
+ my_bool reconnect;
422
+ GET_CLIENT(self)
423
+
424
+ if(!NIL_P(value)) {
425
+ reconnect = value == Qfalse ? 0 : 1;
426
+
427
+ /* set default reconnect behavior */
428
+ if (mysql_options(client, MYSQL_OPT_RECONNECT, &reconnect)) {
429
+ /* TODO: warning - unable to set reconnect behavior */
430
+ rb_warn("%s\n", mysql_error(client));
431
+ }
432
+ }
433
+ return value;
434
+ }
435
+
436
+ static VALUE set_connect_timeout(VALUE self, VALUE value) {
437
+ unsigned int connect_timeout = 0;
438
+ GET_CLIENT(self)
439
+
440
+ if(!NIL_P(value)) {
441
+ connect_timeout = NUM2INT(value);
442
+ if(0 == connect_timeout) return value;
443
+
444
+ /* set default connection timeout behavior */
445
+ if (mysql_options(client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout)) {
446
+ /* TODO: warning - unable to set connection timeout */
447
+ rb_warn("%s\n", mysql_error(client));
448
+ }
449
+ }
450
+ return value;
451
+ }
452
+
453
+ static VALUE set_charset_name(VALUE self, VALUE value) {
454
+ char * charset_name;
455
+ GET_CLIENT(self)
456
+
457
+ #ifdef HAVE_RUBY_ENCODING_H
458
+ VALUE new_encoding;
459
+ new_encoding = rb_funcall(cMysql2Client, intern_encoding_from_charset, 1, value);
460
+ if (new_encoding == Qnil) {
461
+ rb_raise(cMysql2Error, "Unsupported charset: '%s'", RSTRING_PTR(value));
462
+ } else {
463
+ if (wrapper->encoding == Qnil) {
464
+ wrapper->encoding = new_encoding;
465
+ }
466
+ }
467
+ #endif
468
+
469
+ charset_name = StringValuePtr(value);
470
+
471
+ if (mysql_options(client, MYSQL_SET_CHARSET_NAME, charset_name)) {
472
+ /* TODO: warning - unable to set charset */
473
+ rb_warn("%s\n", mysql_error(client));
474
+ }
475
+
476
+ return value;
477
+ }
478
+
479
+ static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE capath, VALUE cipher) {
480
+ GET_CLIENT(self)
481
+
482
+ if(!NIL_P(ca) || !NIL_P(key)) {
483
+ mysql_ssl_set(client,
484
+ NIL_P(key) ? NULL : StringValuePtr(key),
485
+ NIL_P(cert) ? NULL : StringValuePtr(cert),
486
+ NIL_P(ca) ? NULL : StringValuePtr(ca),
487
+ NIL_P(capath) ? NULL : StringValuePtr(capath),
488
+ NIL_P(cipher) ? NULL : StringValuePtr(cipher));
489
+ }
490
+
491
+ return self;
492
+ }
493
+
494
+ static VALUE init_connection(VALUE self) {
495
+ GET_CLIENT(self)
496
+
497
+ if (rb_thread_blocking_region(nogvl_init, client, RUBY_UBF_IO, 0) == Qfalse) {
498
+ /* TODO: warning - not enough memory? */
499
+ return rb_raise_mysql2_error(client);
500
+ }
501
+
502
+ return self;
503
+ }
504
+
505
+ void init_mysql2_client() {
506
+ cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject);
507
+
508
+ rb_define_alloc_func(cMysql2Client, allocate);
509
+
510
+ rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0);
511
+ rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1);
512
+ rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1);
513
+ rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0);
514
+ rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0);
515
+ rb_define_method(cMysql2Client, "socket", rb_mysql_client_socket, 0);
516
+ rb_define_method(cMysql2Client, "async_result", rb_mysql_client_async_result, 0);
517
+ rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0);
518
+ rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0);
519
+
520
+ rb_define_private_method(cMysql2Client, "reconnect=", set_reconnect, 1);
521
+ rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1);
522
+ rb_define_private_method(cMysql2Client, "charset_name=", set_charset_name, 1);
523
+ rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5);
524
+ rb_define_private_method(cMysql2Client, "init_connection", init_connection, 0);
525
+ rb_define_private_method(cMysql2Client, "connect", rb_connect, 6);
526
+
527
+ intern_encoding_from_charset = rb_intern("encoding_from_charset");
528
+
529
+ sym_id = ID2SYM(rb_intern("id"));
530
+ sym_version = ID2SYM(rb_intern("version"));
531
+ sym_async = ID2SYM(rb_intern("async"));
532
+ sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys"));
533
+ sym_as = ID2SYM(rb_intern("as"));
534
+ sym_array = ID2SYM(rb_intern("array"));
535
+
536
+ intern_merge = rb_intern("merge");
537
+ intern_error_number_eql = rb_intern("error_number=");
538
+ intern_sql_state_eql = rb_intern("sql_state=");
539
+
540
+ }