mysql2 0.1.9 → 0.2.0

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