From b3ec7b57defd84fd70c630ec488250756fcc7422 Mon Sep 17 00:00:00 2001 From: Luis Lavena Date: Sat, 21 Aug 2010 22:38:21 -0300 Subject: [PATCH 01/23] Bumped MySQL version 5.1.50 for Windows And also cleanup compiled extension when 'rake clean' --- tasks/compile.rake | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tasks/compile.rake b/tasks/compile.rake index 712eb17..d767bfe 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -1,8 +1,8 @@ gem 'rake-compiler', '~> 0.7.1' require "rake/extensiontask" -MYSQL_VERSION = "5.1.49" -MYSQL_MIRROR = ENV['MYSQL_MIRROR'] || "http://mysql.localhost.net.ar" +MYSQL_VERSION = "5.1.50" +MYSQL_MIRROR = ENV['MYSQL_MIRROR'] || "http://mysql.mirrors.pair.com" Rake::ExtensionTask.new("mysql2", JEWELER.gemspec) do |ext| # reference where the vendored MySQL got extracted @@ -15,5 +15,8 @@ Rake::ExtensionTask.new("mysql2", JEWELER.gemspec) do |ext| end ext.lib_dir = File.join 'lib', 'mysql2' + + # clean compiled extension + CLEAN.include "#{ext.lib_dir}/*.#{RbConfig::CONFIG['DLEXT']}" end Rake::Task[:spec].prerequisites << :compile From 4f9625f877a950cbcf68f8ab73414d899508f454 Mon Sep 17 00:00:00 2001 From: Luis Lavena Date: Sat, 21 Aug 2010 22:39:41 -0300 Subject: [PATCH 02/23] Wrap fcntl to exclude Windows. Naive implementation, get it compiled but crashes. --- ext/mysql2/client.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 2a4d643..c6a9dab 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -117,16 +117,20 @@ static void rb_mysql_client_free(void * ptr) { * anyways, so ensure the socket write does not block our interpreter */ int fd = client->net.fd; - int flags; if (fd >= 0) { /* * if the socket is dead we have no chance of blocking, * so ignore any potential fcntl errors since they don't matter */ - flags = fcntl(fd, F_GETFL); +#ifndef _WIN32 + int flags = fcntl(fd, F_GETFL); if (flags > 0 && !(flags & O_NONBLOCK)) fcntl(fd, F_SETFL, flags | O_NONBLOCK); +#else + u_long iMode = 1; + ioctlsocket(fd, FIONBIO, &iMode); +#endif } /* It's safe to call mysql_close() on an already closed connection. */ From 1100288eba30ceb243bcef5a09219c3f9fbda89e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 24 Aug 2010 04:07:55 +0000 Subject: [PATCH 03/23] avoid stack overflow when escaping large strings Attempting to escape large, untrusted strings cause stack overflows (easier under Ruby 1.9 using pthreads) leading to SystemStackError on small overflows and segmentation faults on large overflows. Instead of allocating on the stack, we'll allocate a string buffer from the heap. This has the unfortunate effect of reducing escape performance for common cases, but in my experience SQL escaping isn't much of a bottleneck. For reference, Ruby 1.9.2-p0 with pthreads gets a stack size of 512K and the default process stack size is 8MB on both x86 and x86_64 Linux. --- ext/mysql2/client.c | 6 +++--- spec/mysql2/client_spec.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index c6a9dab..f6c947a 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -342,15 +342,15 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { #endif oldLen = RSTRING_LEN(str); - char escaped[(oldLen*2)+1]; + newStr = rb_str_new(0, oldLen*2+1); REQUIRE_OPEN_DB(client); - newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), oldLen); + newLen = mysql_real_escape_string(client, RSTRING_PTR(newStr), StringValuePtr(str), oldLen); if (newLen == oldLen) { // no need to return a new ruby string if nothing changed return str; } else { - newStr = rb_str_new(escaped, newLen); + rb_str_resize(newStr, newLen); #ifdef HAVE_RUBY_ENCODING_H rb_enc_associate(newStr, conn_enc); if (default_internal_enc) { diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index b3ffcbc..936e1ea 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -144,6 +144,18 @@ describe Mysql2::Client do @client.escape(str).object_id.should eql(str.object_id) end + it "#escape should not overflow the thread stack" do + lambda { + Thread.new { @client.escape("'" * 256 * 1024) }.join + }.should_not raise_error(SystemStackError) + end + + it "#escape should not overflow the process stack" do + lambda { + Thread.new { @client.escape("'" * 1024 * 1024 * 4) }.join + }.should_not raise_error(SystemStackError) + end + it "should respond to #info" do @client.should respond_to(:info) end From 3b2e7602a0baaf8381a44b52a3858a570cb2cb5c Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 24 Aug 2010 09:27:29 -0700 Subject: [PATCH 04/23] formatting fix so I collapse in TM --- spec/mysql2/client_spec.rb | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 936e1ea..cad599e 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -105,30 +105,32 @@ describe Mysql2::Client do # XXX this test is not deterministic (because Unix signal handling is not) # and may fail on a loaded system - it "should run signal handlers while waiting for a response" do - mark = {} - trap(:USR1) { mark[:USR1] = Time.now } - begin - mark[:START] = Time.now - pid = fork do - sleep 1 # wait for client "SELECT sleep(2)" query to start - Process.kill(:USR1, Process.ppid) - sleep # wait for explicit kill to prevent GC disconnect + if RUBY_PLATFORM !~ /mingw|mswin/ + it "should run signal handlers while waiting for a response" do + mark = {} + trap(:USR1) { mark[:USR1] = Time.now } + begin + mark[:START] = Time.now + pid = fork do + sleep 1 # wait for client "SELECT sleep(2)" query to start + Process.kill(:USR1, Process.ppid) + sleep # wait for explicit kill to prevent GC disconnect + end + @client.query("SELECT sleep(2)") + mark[:END] = Time.now + mark.include?(:USR1).should be_true + (mark[:USR1] - mark[:START]).should >= 1 + (mark[:USR1] - mark[:START]).should < 1.1 + (mark[:END] - mark[:USR1]).should > 0.9 + (mark[:END] - mark[:START]).should >= 2 + (mark[:END] - mark[:START]).should < 2.1 + Process.kill(:TERM, pid) + Process.waitpid2(pid) + ensure + trap(:USR1, 'DEFAULT') end - @client.query("SELECT sleep(2)") - mark[:END] = Time.now - mark.include?(:USR1).should be_true - (mark[:USR1] - mark[:START]).should >= 1 - (mark[:USR1] - mark[:START]).should < 1.1 - (mark[:END] - mark[:USR1]).should > 0.9 - (mark[:END] - mark[:START]).should >= 2 - (mark[:END] - mark[:START]).should < 2.1 - Process.kill(:TERM, pid) - Process.waitpid2(pid) - ensure - trap(:USR1, 'DEFAULT') end - end if RUBY_PLATFORM !~ /mingw|mswin/ + end end it "should respond to #escape" do From 10222fb4552b42e125899e118f6ee0b05161e622 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 24 Aug 2010 20:59:17 -0700 Subject: [PATCH 05/23] appease the rdoc.info godz --- README.rdoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rdoc b/README.rdoc index 9398586..ee6356b 100644 --- a/README.rdoc +++ b/README.rdoc @@ -89,15 +89,15 @@ or === Array of Arrays -Pass the {:as => :array} option to any of the above methods of configuration +Pass the :as => :array option to any of the above methods of configuration === Array of Hashes -The default result type is set to :hash, but you can override a previous setting to something else with {:as => :hash} +The default result type is set to :hash, but you can override a previous setting to something else with :as => :hash === Others... -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. +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. If you'd like to see either of these (or others), open an issue and start bugging me about it ;) == Timezones From ae6c33a13fe9993bcf7374f5572b92e0c2f1dbba Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 27 Aug 2010 12:08:48 -0700 Subject: [PATCH 06/23] add cache_rows option to enable/disable internal row caching for results --- README.rdoc | 16 +++++++++++++--- ext/mysql2/result.c | 17 ++++++++++++----- lib/mysql2/client.rb | 13 +++++++------ spec/mysql2/result_spec.rb | 9 +++++++-- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.rdoc b/README.rdoc index ee6356b..a8df4a5 100644 --- a/README.rdoc +++ b/README.rdoc @@ -100,7 +100,7 @@ The default result type is set to :hash, but you can override a previous setting 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. If you'd like to see either of these (or others), open an issue and start bugging me about it ;) -== Timezones +=== Timezones Mysql2 now supports two timezone options: @@ -112,14 +112,14 @@ Then, if :application_timezone is set to say - :local - Mysql2 will then convert Both options only allow two values - :local or :utc - with the exception that :application_timezone can be [and defaults to] nil -== Casting "boolean" columns +=== Casting "boolean" columns You can now tell Mysql2 to cast tinyint(1) fields to boolean values in Ruby with the :cast_booleans option. client = Mysql2::Client.new result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true) -== Async +=== Async Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries. But, in order to take full advantage of it in your Ruby code, you can do: @@ -136,6 +136,14 @@ NOTE: Because of the way MySQL's query API works, this method will block until t So if you really need things to stay async, it's best to just monitor the socket with something like EventMachine. If you need multiple query concurrency take a look at using a connection pool. +=== Row Caching + +By default, Mysql2 will cache rows that have been created in Ruby (since this happens lazily). +This is especially helpful since it saves the cost of creating the row in Ruby if you were to iterate over the collection again. + +If you only plan on using each row once, then it's much more efficient to disable this behavior by setting the :cache_rows option to false. +This would be helpful if you wanted to iterate over the results in a streaming manner. Meaning the GC would cleanup rows you don't need anymore as you're iterating over the result set. + == ActiveRecord 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". @@ -182,6 +190,8 @@ For example, if you were to yield 4 rows from a 100 row dataset, only 4 hashes w Now say you were to iterate over that same collection again, this time yielding 15 rows - the 4 previous rows that had already been turned into ruby hashes would be pulled from an internal cache, then 11 more would be created and stored in that cache. Once the entire dataset has been converted into ruby objects, Mysql2::Result will free the Mysql C result object as it's no longer needed. +This caching behavior can be disabled by setting the :cache_rows option to false. + As for field values themselves, I'm workin on it - but expect that soon. == Compatibility diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 5c7302e..2cfaa5d 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -12,7 +12,7 @@ static VALUE intern_encoding_from_charset; static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code, intern_localtime, intern_local_offset, intern_civil, intern_new_offset; static ID sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_application_timezone, - sym_local, sym_utc, sym_cast_booleans; + sym_local, sym_utc, sym_cast_booleans, sym_cache_rows; static ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { @@ -316,7 +316,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { ID db_timezone, app_timezone, dbTz, appTz; mysql2_result_wrapper * wrapper; unsigned long i; - int symbolizeKeys = 0, asArray = 0, castBool = 0; + int symbolizeKeys = 0, asArray = 0, castBool = 0, cacheRows = 1; GetMysql2Result(self, wrapper); @@ -339,6 +339,10 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { castBool = 1; } + if (rb_hash_aref(opts, sym_cache_rows) == Qfalse) { + cacheRows = 0; + } + dbTz = rb_hash_aref(opts, sym_database_timezone); if (dbTz == sym_local) { db_timezone = intern_local; @@ -369,7 +373,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { wrapper->rows = rb_ary_new2(wrapper->numberOfRows); } - if (wrapper->lastRowProcessed == wrapper->numberOfRows) { + if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) { // we've already read the entire dataset from the C result into our // internal array. Lets hand that over to the user since it's ready to go for (i = 0; i < wrapper->numberOfRows; i++) { @@ -380,11 +384,13 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { rowsProcessed = RARRAY_LEN(wrapper->rows); for (i = 0; i < wrapper->numberOfRows; i++) { VALUE row; - if (i < rowsProcessed) { + if (cacheRows && i < rowsProcessed) { row = rb_ary_entry(wrapper->rows, i); } else { row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool); - rb_ary_store(wrapper->rows, i, row); + if (cacheRows) { + rb_ary_store(wrapper->rows, i, row); + } wrapper->lastRowProcessed++; } @@ -453,6 +459,7 @@ void init_mysql2_result() { sym_cast_booleans = ID2SYM(rb_intern("cast_booleans")); sym_database_timezone = ID2SYM(rb_intern("database_timezone")); sym_application_timezone = ID2SYM(rb_intern("application_timezone")); + sym_cache_rows = ID2SYM(rb_intern("cache_rows")); rb_global_variable(&opt_decimal_zero); //never GC opt_decimal_zero = rb_str_new2("0.0"); diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index 732508d..bf55621 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -2,12 +2,13 @@ module Mysql2 class Client attr_reader :query_options @@default_query_options = { - :as => :hash, - :async => false, - :cast_booleans => false, - :symbolize_keys => false, - :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in - :application_timezone => nil # timezone Mysql2 will convert to before handing the object back to the caller + :as => :hash, # the type of object you want each row back as; also supports :array (an array of values) + :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result + :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby + :symbolize_keys => false, # return field names as symbols instead of strings + :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in + :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller + :cache_rows => true # tells Mysql2 to use it's internal row cache for results } def initialize(opts = {}) diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index ee3820d..9491572 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -47,8 +47,13 @@ describe Mysql2::Result do end end - it "should cache previously yielded results" do - @result.first.should eql(@result.first) + it "should cache previously yielded results by default" do + @result.first.object_id.should eql(@result.first.object_id) + end + + it "should not cache previously yielded results if cache_rows is disabled" do + result = @client.query "SELECT 1", :cache_rows => false + result.first.object_id.should_not eql(result.first.object_id) end end From 98fbeb6f64e562e9889346d46737ad2d5d5cf6fe Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 1 Sep 2010 11:42:16 -0700 Subject: [PATCH 07/23] add aliases for Mysql compatibility --- lib/mysql2/error.rb | 4 ++++ spec/mysql2/error_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/mysql2/error.rb b/lib/mysql2/error.rb index c14ae35..e195c2c 100644 --- a/lib/mysql2/error.rb +++ b/lib/mysql2/error.rb @@ -7,5 +7,9 @@ module Mysql2 @error_number = nil @sql_state = nil end + + # Mysql gem compatibility + alias_method :errno, :error_number + alias_method :error, :message end end diff --git a/spec/mysql2/error_spec.rb b/spec/mysql2/error_spec.rb index f321003..9690744 100644 --- a/spec/mysql2/error_spec.rb +++ b/spec/mysql2/error_spec.rb @@ -13,4 +13,13 @@ describe Mysql2::Error do it "should respond to #sql_state" do @error.should respond_to(:sql_state) end + + # Mysql gem compatibility + it "should alias #error_number to #errno" do + @error.should respond_to(:errno) + end + + it "should alias #message to #error" do + @error.should respond_to(:error) + end end From 9b401ba9b97cbbd18b1e68442f0f9ce70478f497 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 4 Sep 2010 10:08:24 -0700 Subject: [PATCH 08/23] take advantage of DO API in benchmark --- benchmark/query_with_mysql_casting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/query_with_mysql_casting.rb b/benchmark/query_with_mysql_casting.rb index f8b6dd8..22adb5a 100644 --- a/benchmark/query_with_mysql_casting.rb +++ b/benchmark/query_with_mysql_casting.rb @@ -70,7 +70,7 @@ Benchmark.bmbm do |x| end do_mysql = DataObjects::Connection.new("mysql://localhost/#{database}") - command = DataObjects::Mysql::Command.new do_mysql, sql + command = do_mysql.create_command sql x.report do puts "do_mysql" number_of.times do From c110d0d2635d7deefb1973b10fb6880f66578a01 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 4 Sep 2010 11:50:54 -0700 Subject: [PATCH 09/23] rbx doesn't have rb_obj_dup yet --- ext/mysql2/client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index f6c947a..432d9e3 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -248,7 +248,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) { VALUE resultObj = rb_mysql_result_to_obj(result); // pass-through query options for result construction later - rb_iv_set(resultObj, "@query_options", rb_obj_dup(rb_iv_get(self, "@query_options"))); + rb_iv_set(resultObj, "@query_options", rb_funcall(rb_iv_get(self, "@query_options"), rb_intern("dup"), 0)); #ifdef HAVE_RUBY_ENCODING_H mysql2_result_wrapper * result_wrapper; From d12c5e2e27ae444718474a1cd0bbdcdfc6171aa2 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 4 Sep 2010 11:51:55 -0700 Subject: [PATCH 10/23] remove cached values for better rbx compatibility (this may come back in the near future) --- ext/mysql2/client.c | 1 + ext/mysql2/result.c | 25 ++++--------------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 432d9e3..8943443 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -305,6 +305,7 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { // the below code is largely from do_mysql // http://github.com/datamapper/do fd = client->net.fd; + for(;;) { FD_ZERO(&fdset); FD_SET(fd, &fdset); diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 2cfaa5d..89d24d0 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -6,7 +6,6 @@ rb_encoding *binaryEncoding; VALUE cMysql2Result; VALUE cBigDecimal, cDate, cDateTime; -VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset; extern VALUE mMysql2, cMysql2Client, cMysql2Error; static VALUE intern_encoding_from_charset; static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code, @@ -149,27 +148,19 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo break; case MYSQL_TYPE_DECIMAL: // DECIMAL or NUMERIC field case MYSQL_TYPE_NEWDECIMAL: // Precision math DECIMAL or NUMERIC field (MySQL 5.0.3 and up) - if (strtod(row[i], NULL) == 0.000000){ - val = rb_funcall(cBigDecimal, intern_new, 1, opt_decimal_zero); - }else{ - val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(row[i], fieldLengths[i])); - } + val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(row[i], fieldLengths[i])); break; case MYSQL_TYPE_FLOAT: // FLOAT field case MYSQL_TYPE_DOUBLE: { // DOUBLE or REAL field double column_to_double; column_to_double = strtod(row[i], NULL); - if (column_to_double == 0.000000){ - val = opt_float_zero; - }else{ - val = rb_float_new(column_to_double); - } + val = rb_float_new(column_to_double); break; } case MYSQL_TYPE_TIME: { // TIME field int hour, min, sec, tokens; tokens = sscanf(row[i], "%2d:%2d:%2d", &hour, &min, &sec); - val = rb_funcall(rb_cTime, db_timezone, 6, opt_time_year, opt_time_month, opt_time_month, INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, db_timezone, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); if (!NIL_P(app_timezone)) { if (app_timezone == intern_local) { val = rb_funcall(val, intern_localtime, 0); @@ -201,7 +192,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo offset = rb_funcall(cMysql2Client, intern_local_offset, 0); val = rb_funcall(val, intern_new_offset, 1, offset); } else { // utc - val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset); + val = rb_funcall(val, intern_new_offset, 1, INT2NUM(0)); } } } else { @@ -461,14 +452,6 @@ void init_mysql2_result() { sym_application_timezone = ID2SYM(rb_intern("application_timezone")); sym_cache_rows = ID2SYM(rb_intern("cache_rows")); - rb_global_variable(&opt_decimal_zero); //never GC - opt_decimal_zero = rb_str_new2("0.0"); - rb_global_variable(&opt_float_zero); - opt_float_zero = rb_float_new((double)0); - opt_time_year = INT2NUM(2000); - opt_time_month = INT2NUM(1); - opt_utc_offset = INT2NUM(0); - #ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); #endif From 530b082905db7c49b027bcac242be3cdb2986e2f Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 7 Sep 2010 15:38:08 -0700 Subject: [PATCH 11/23] Revert "remove cached values for better rbx compatibility (this may come back in the near future)" This reverts commit d12c5e2e27ae444718474a1cd0bbdcdfc6171aa2. --- ext/mysql2/client.c | 1 - ext/mysql2/result.c | 25 +++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 8943443..432d9e3 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -305,7 +305,6 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { // the below code is largely from do_mysql // http://github.com/datamapper/do fd = client->net.fd; - for(;;) { FD_ZERO(&fdset); FD_SET(fd, &fdset); diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 89d24d0..2cfaa5d 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -6,6 +6,7 @@ rb_encoding *binaryEncoding; VALUE cMysql2Result; VALUE cBigDecimal, cDate, cDateTime; +VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset; extern VALUE mMysql2, cMysql2Client, cMysql2Error; static VALUE intern_encoding_from_charset; static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code, @@ -148,19 +149,27 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo break; case MYSQL_TYPE_DECIMAL: // DECIMAL or NUMERIC field case MYSQL_TYPE_NEWDECIMAL: // Precision math DECIMAL or NUMERIC field (MySQL 5.0.3 and up) - val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(row[i], fieldLengths[i])); + if (strtod(row[i], NULL) == 0.000000){ + val = rb_funcall(cBigDecimal, intern_new, 1, opt_decimal_zero); + }else{ + val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(row[i], fieldLengths[i])); + } break; case MYSQL_TYPE_FLOAT: // FLOAT field case MYSQL_TYPE_DOUBLE: { // DOUBLE or REAL field double column_to_double; column_to_double = strtod(row[i], NULL); - val = rb_float_new(column_to_double); + if (column_to_double == 0.000000){ + val = opt_float_zero; + }else{ + val = rb_float_new(column_to_double); + } break; } case MYSQL_TYPE_TIME: { // TIME field int hour, min, sec, tokens; tokens = sscanf(row[i], "%2d:%2d:%2d", &hour, &min, &sec); - val = rb_funcall(rb_cTime, db_timezone, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, db_timezone, 6, opt_time_year, opt_time_month, opt_time_month, INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); if (!NIL_P(app_timezone)) { if (app_timezone == intern_local) { val = rb_funcall(val, intern_localtime, 0); @@ -192,7 +201,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo offset = rb_funcall(cMysql2Client, intern_local_offset, 0); val = rb_funcall(val, intern_new_offset, 1, offset); } else { // utc - val = rb_funcall(val, intern_new_offset, 1, INT2NUM(0)); + val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset); } } } else { @@ -452,6 +461,14 @@ void init_mysql2_result() { sym_application_timezone = ID2SYM(rb_intern("application_timezone")); sym_cache_rows = ID2SYM(rb_intern("cache_rows")); + rb_global_variable(&opt_decimal_zero); //never GC + opt_decimal_zero = rb_str_new2("0.0"); + rb_global_variable(&opt_float_zero); + opt_float_zero = rb_float_new((double)0); + opt_time_year = INT2NUM(2000); + opt_time_month = INT2NUM(1); + opt_utc_offset = INT2NUM(0); + #ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); #endif From 64be4019ad1b2371b5591004ac90fe031b5f97a7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 7 Sep 2010 15:43:22 -0700 Subject: [PATCH 12/23] work around an rbx bug (it's going to be fixed in rbx soon anyhow) --- ext/mysql2/result.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 2cfaa5d..7375df5 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -461,10 +461,10 @@ void init_mysql2_result() { sym_application_timezone = ID2SYM(rb_intern("application_timezone")); sym_cache_rows = ID2SYM(rb_intern("cache_rows")); - rb_global_variable(&opt_decimal_zero); //never GC opt_decimal_zero = rb_str_new2("0.0"); - rb_global_variable(&opt_float_zero); + rb_global_variable(&opt_decimal_zero); //never GC opt_float_zero = rb_float_new((double)0); + rb_global_variable(&opt_float_zero); opt_time_year = INT2NUM(2000); opt_time_month = INT2NUM(1); opt_utc_offset = INT2NUM(0); From b1b66cc389f72c529d3d37a5ccf0e076fcbdbcf1 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 9 Sep 2010 07:35:31 -0700 Subject: [PATCH 13/23] enable cross-compilation --- ext/mysql2/result.c | 4 ++-- ext/mysql2/result.h | 2 +- tasks/compile.rake | 12 +++++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 7375df5..a78c9e3 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -95,7 +95,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo mysql2_result_wrapper * wrapper; MYSQL_ROW row; MYSQL_FIELD * fields = NULL; - unsigned int i = 0; + long i = 0; unsigned long * fieldLengths; void * ptr; @@ -286,7 +286,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo static VALUE rb_mysql_result_fetch_fields(VALUE self) { mysql2_result_wrapper * wrapper; - unsigned int i = 0; + long i = 0; short int symbolizeKeys = 0; VALUE defaults; diff --git a/ext/mysql2/result.h b/ext/mysql2/result.h index dd47ced..c637d9e 100644 --- a/ext/mysql2/result.h +++ b/ext/mysql2/result.h @@ -8,7 +8,7 @@ typedef struct { VALUE fields; VALUE rows; VALUE encoding; - unsigned int numberOfFields; + long numberOfFields; unsigned long numberOfRows; unsigned long lastRowProcessed; short int resultFreed; diff --git a/tasks/compile.rake b/tasks/compile.rake index d767bfe..7979ae2 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -4,7 +4,12 @@ require "rake/extensiontask" MYSQL_VERSION = "5.1.50" MYSQL_MIRROR = ENV['MYSQL_MIRROR'] || "http://mysql.mirrors.pair.com" -Rake::ExtensionTask.new("mysql2", JEWELER.gemspec) do |ext| + +def gemspec + @clean_gemspec ||= eval(File.read(File.expand_path('../../mysql2.gemspec', __FILE__))) +end + +Rake::ExtensionTask.new("mysql2", gemspec) do |ext| # reference where the vendored MySQL got extracted mysql_lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'vendor', "mysql-#{MYSQL_VERSION}-win32")) @@ -12,6 +17,11 @@ Rake::ExtensionTask.new("mysql2", JEWELER.gemspec) do |ext| if RUBY_PLATFORM =~ /mswin|mingw/ then ext.config_options << "--with-mysql-include=#{mysql_lib}/include" ext.config_options << "--with-mysql-lib=#{mysql_lib}/lib/opt" + else + ext.cross_compile = true + ext.cross_platform = ['x86-mingw32', 'x86-mswin32-60'] + ext.cross_config_options << "--with-mysql-include=#{mysql_lib}/include" + ext.cross_config_options << "--with-mysql-lib=#{mysql_lib}/lib/opt" end ext.lib_dir = File.join 'lib', 'mysql2' From 0f0bd5a0011f6324f16b272fc33d074e6354cc59 Mon Sep 17 00:00:00 2001 From: Kouhei Yanagita Date: Sat, 4 Sep 2010 20:39:55 +0800 Subject: [PATCH 14/23] set IndexDefinition#length --- lib/active_record/connection_adapters/mysql2_adapter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index e4ab71d..78a7901 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -447,10 +447,11 @@ module ActiveRecord if current_index != row[:Key_name] next if row[:Key_name] == PRIMARY # skip the primary key current_index = row[:Key_name] - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, []) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) end indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] end indexes end From 7b8d6359c2d2c4e943635a7d459bd7fb2b40599c Mon Sep 17 00:00:00 2001 From: Joe Damato Date: Tue, 14 Sep 2010 18:01:14 -0700 Subject: [PATCH 15/23] Fix data corruption bug --- ext/mysql2/client.c | 84 +++++++++++++++++++++------------------------ ext/mysql2/client.h | 2 +- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 432d9e3..7fe8ba8 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -19,9 +19,7 @@ static ID intern_merge, intern_error_number_eql, intern_sql_state_eql; #define GET_CLIENT(self) \ mysql_client_wrapper *wrapper; \ - MYSQL *client; \ - Data_Get_Struct(self, mysql_client_wrapper, wrapper); \ - client = &wrapper->client; + Data_Get_Struct(self, mysql_client_wrapper, wrapper); /* * used to pass all arguments to mysql_real_connect while inside @@ -85,12 +83,11 @@ static VALUE rb_raise_mysql2_error(MYSQL *client) { } static VALUE nogvl_init(void *ptr) { - MYSQL * client = (MYSQL *)ptr; + MYSQL **client = (MYSQL **)ptr; /* may initialize embedded server and read /etc/services off disk */ - client = mysql_init(NULL); - - return client ? Qtrue : Qfalse; + *client = mysql_init(NULL); + return *client ? Qtrue : Qfalse; } static VALUE nogvl_connect(void *ptr) { @@ -108,15 +105,14 @@ static VALUE nogvl_connect(void *ptr) { } static void rb_mysql_client_free(void * ptr) { - mysql_client_wrapper * wrapper = (mysql_client_wrapper *)ptr; - MYSQL * client = &wrapper->client; + GET_CLIENT(ptr) /* * we'll send a QUIT message to the server, but that message is more of a * formality than a hard requirement since the socket is getting shutdown * anyways, so ensure the socket write does not block our interpreter */ - int fd = client->net.fd; + int fd = wrapper->client->net.fd; if (fd >= 0) { /* @@ -134,7 +130,7 @@ static void rb_mysql_client_free(void * ptr) { } /* It's safe to call mysql_close() on an already closed connection. */ - mysql_close(client); + mysql_close(wrapper->client); xfree(ptr); } @@ -164,12 +160,12 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po args.user = NIL_P(user) ? NULL : StringValuePtr(user); args.passwd = NIL_P(pass) ? NULL : StringValuePtr(pass); args.db = NIL_P(database) ? NULL : StringValuePtr(database); - args.mysql = client; + args.mysql = wrapper->client; args.client_flag = NUM2INT(flags); if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) { // unable to connect - return rb_raise_mysql2_error(client); + return rb_raise_mysql2_error(wrapper->client); } return self; @@ -184,7 +180,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po static VALUE rb_mysql_client_close(VALUE self) { GET_CLIENT(self) - rb_thread_blocking_region(nogvl_close, client, RUBY_UBF_IO, 0); + rb_thread_blocking_region(nogvl_close, wrapper->client, RUBY_UBF_IO, 0); return Qnil; } @@ -227,21 +223,21 @@ static VALUE rb_mysql_client_async_result(VALUE self) { MYSQL_RES * result; GET_CLIENT(self) - REQUIRE_OPEN_DB(client); - if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { + REQUIRE_OPEN_DB(wrapper->client); + if (rb_thread_blocking_region(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { // an error occurred, mark this connection inactive MARK_CONN_INACTIVE(self); - return rb_raise_mysql2_error(client); + return rb_raise_mysql2_error(wrapper->client); } - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, client, RUBY_UBF_IO, 0); + result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, wrapper->client, RUBY_UBF_IO, 0); // we have our result, mark this connection inactive MARK_CONN_INACTIVE(self); if (result == NULL) { - if (mysql_field_count(client) != 0) { - rb_raise_mysql2_error(client); + if (mysql_field_count(wrapper->client) != 0) { + rb_raise_mysql2_error(wrapper->client); } return Qnil; } @@ -266,8 +262,8 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { VALUE opts, defaults; GET_CLIENT(self) - REQUIRE_OPEN_DB(client); - args.mysql = client; + REQUIRE_OPEN_DB(wrapper->client); + args.mysql = wrapper->client; // see if this connection is still waiting on a result from a previous query if (wrapper->active == 0) { @@ -298,13 +294,13 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) { // an error occurred, we're not active anymore MARK_CONN_INACTIVE(self); - return rb_raise_mysql2_error(client); + return rb_raise_mysql2_error(wrapper->client); } if (!async) { // the below code is largely from do_mysql // http://github.com/datamapper/do - fd = client->net.fd; + fd = wrapper->client->net.fd; for(;;) { FD_ZERO(&fdset); FD_SET(fd, &fdset); @@ -344,8 +340,8 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { oldLen = RSTRING_LEN(str); newStr = rb_str_new(0, oldLen*2+1); - REQUIRE_OPEN_DB(client); - newLen = mysql_real_escape_string(client, RSTRING_PTR(newStr), StringValuePtr(str), oldLen); + REQUIRE_OPEN_DB(wrapper->client); + newLen = mysql_real_escape_string(wrapper->client, RSTRING_PTR(newStr), StringValuePtr(str), oldLen); if (newLen == oldLen) { // no need to return a new ruby string if nothing changed return str; @@ -389,11 +385,11 @@ static VALUE rb_mysql_client_server_info(VALUE self) { rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); #endif - REQUIRE_OPEN_DB(client); + REQUIRE_OPEN_DB(wrapper->client); version = rb_hash_new(); - rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client))); - server_info = rb_str_new2(mysql_get_server_info(client)); + rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(wrapper->client))); + server_info = rb_str_new2(mysql_get_server_info(wrapper->client)); #ifdef HAVE_RUBY_ENCODING_H rb_enc_associate(server_info, conn_enc); if (default_internal_enc) { @@ -406,20 +402,20 @@ static VALUE rb_mysql_client_server_info(VALUE self) { static VALUE rb_mysql_client_socket(VALUE self) { GET_CLIENT(self) - REQUIRE_OPEN_DB(client); - return INT2NUM(client->net.fd); + REQUIRE_OPEN_DB(wrapper->client); + return INT2NUM(wrapper->client->net.fd); } static VALUE rb_mysql_client_last_id(VALUE self) { GET_CLIENT(self) - REQUIRE_OPEN_DB(client); - return ULL2NUM(mysql_insert_id(client)); + REQUIRE_OPEN_DB(wrapper->client); + return ULL2NUM(mysql_insert_id(wrapper->client)); } static VALUE rb_mysql_client_affected_rows(VALUE self) { GET_CLIENT(self) - REQUIRE_OPEN_DB(client); - return ULL2NUM(mysql_affected_rows(client)); + REQUIRE_OPEN_DB(wrapper->client); + return ULL2NUM(mysql_affected_rows(wrapper->client)); } static VALUE set_reconnect(VALUE self, VALUE value) { @@ -430,9 +426,9 @@ static VALUE set_reconnect(VALUE self, VALUE value) { reconnect = value == Qfalse ? 0 : 1; /* set default reconnect behavior */ - if (mysql_options(client, MYSQL_OPT_RECONNECT, &reconnect)) { + if (mysql_options(wrapper->client, MYSQL_OPT_RECONNECT, &reconnect)) { /* TODO: warning - unable to set reconnect behavior */ - rb_warn("%s\n", mysql_error(client)); + rb_warn("%s\n", mysql_error(wrapper->client)); } } return value; @@ -447,9 +443,9 @@ static VALUE set_connect_timeout(VALUE self, VALUE value) { if(0 == connect_timeout) return value; /* set default connection timeout behavior */ - if (mysql_options(client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout)) { + if (mysql_options(wrapper->client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout)) { /* TODO: warning - unable to set connection timeout */ - rb_warn("%s\n", mysql_error(client)); + rb_warn("%s\n", mysql_error(wrapper->client)); } } return value; @@ -473,9 +469,9 @@ static VALUE set_charset_name(VALUE self, VALUE value) { charset_name = StringValuePtr(value); - if (mysql_options(client, MYSQL_SET_CHARSET_NAME, charset_name)) { + if (mysql_options(wrapper->client, MYSQL_SET_CHARSET_NAME, charset_name)) { /* TODO: warning - unable to set charset */ - rb_warn("%s\n", mysql_error(client)); + rb_warn("%s\n", mysql_error(wrapper->client)); } return value; @@ -485,7 +481,7 @@ static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE GET_CLIENT(self) if(!NIL_P(ca) || !NIL_P(key)) { - mysql_ssl_set(client, + mysql_ssl_set(wrapper->client, NIL_P(key) ? NULL : StringValuePtr(key), NIL_P(cert) ? NULL : StringValuePtr(cert), NIL_P(ca) ? NULL : StringValuePtr(ca), @@ -499,9 +495,9 @@ static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE static VALUE init_connection(VALUE self) { GET_CLIENT(self) - if (rb_thread_blocking_region(nogvl_init, client, RUBY_UBF_IO, 0) == Qfalse) { + if (rb_thread_blocking_region(nogvl_init, ((void *) &wrapper->client), RUBY_UBF_IO, 0) == Qfalse) { /* TODO: warning - not enough memory? */ - return rb_raise_mysql2_error(client); + return rb_raise_mysql2_error(wrapper->client); } return self; diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h index 6bd9963..781ccda 100644 --- a/ext/mysql2/client.h +++ b/ext/mysql2/client.h @@ -34,7 +34,7 @@ void init_mysql2_client(); typedef struct { VALUE encoding; short int active; - MYSQL client; + MYSQL *client; } mysql_client_wrapper; #endif \ No newline at end of file From 05c942a6680b0078534aee130a8d6922f08ba7c5 Mon Sep 17 00:00:00 2001 From: Joe Damato Date: Tue, 14 Sep 2010 19:14:07 -0700 Subject: [PATCH 16/23] Fix macros and associated semicolons --- ext/mysql2/client.c | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 7fe8ba8..4d437b7 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -15,11 +15,11 @@ static ID intern_merge, intern_error_number_eql, intern_sql_state_eql; } #define MARK_CONN_INACTIVE(conn) \ - wrapper->active = 0; + wrapper->active = 0 #define GET_CLIENT(self) \ mysql_client_wrapper *wrapper; \ - Data_Get_Struct(self, mysql_client_wrapper, wrapper); + Data_Get_Struct(self, mysql_client_wrapper, wrapper) /* * used to pass all arguments to mysql_real_connect while inside @@ -105,7 +105,7 @@ static VALUE nogvl_connect(void *ptr) { } static void rb_mysql_client_free(void * ptr) { - GET_CLIENT(ptr) + GET_CLIENT(ptr); /* * we'll send a QUIT message to the server, but that message is more of a @@ -152,7 +152,7 @@ static VALUE allocate(VALUE klass) { static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket, VALUE flags) { struct nogvl_connect_args args; - GET_CLIENT(self) + GET_CLIENT(self); args.host = NIL_P(host) ? "localhost" : StringValuePtr(host); args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket); @@ -178,7 +178,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po * for the garbage collector. */ static VALUE rb_mysql_client_close(VALUE self) { - GET_CLIENT(self) + GET_CLIENT(self); rb_thread_blocking_region(nogvl_close, wrapper->client, RUBY_UBF_IO, 0); @@ -221,7 +221,7 @@ static VALUE nogvl_store_result(void *ptr) { static VALUE rb_mysql_client_async_result(VALUE self) { MYSQL_RES * result; - GET_CLIENT(self) + GET_CLIENT(self); REQUIRE_OPEN_DB(wrapper->client); if (rb_thread_blocking_region(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { @@ -260,7 +260,7 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { int fd, retval; int async = 0; VALUE opts, defaults; - GET_CLIENT(self) + GET_CLIENT(self); REQUIRE_OPEN_DB(wrapper->client); args.mysql = wrapper->client; @@ -327,7 +327,7 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { VALUE newStr; unsigned long newLen, oldLen; - GET_CLIENT(self) + GET_CLIENT(self); Check_Type(str, T_STRING); #ifdef HAVE_RUBY_ENCODING_H @@ -379,7 +379,7 @@ static VALUE rb_mysql_client_info(VALUE self) { static VALUE rb_mysql_client_server_info(VALUE self) { VALUE version, server_info; - GET_CLIENT(self) + GET_CLIENT(self); #ifdef HAVE_RUBY_ENCODING_H rb_encoding *default_internal_enc = rb_default_internal_encoding(); rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); @@ -401,26 +401,26 @@ static VALUE rb_mysql_client_server_info(VALUE self) { } static VALUE rb_mysql_client_socket(VALUE self) { - GET_CLIENT(self) + GET_CLIENT(self); REQUIRE_OPEN_DB(wrapper->client); return INT2NUM(wrapper->client->net.fd); } static VALUE rb_mysql_client_last_id(VALUE self) { - GET_CLIENT(self) + GET_CLIENT(self); REQUIRE_OPEN_DB(wrapper->client); return ULL2NUM(mysql_insert_id(wrapper->client)); } static VALUE rb_mysql_client_affected_rows(VALUE self) { - GET_CLIENT(self) + GET_CLIENT(self); REQUIRE_OPEN_DB(wrapper->client); return ULL2NUM(mysql_affected_rows(wrapper->client)); } static VALUE set_reconnect(VALUE self, VALUE value) { my_bool reconnect; - GET_CLIENT(self) + GET_CLIENT(self); if(!NIL_P(value)) { reconnect = value == Qfalse ? 0 : 1; @@ -436,7 +436,7 @@ static VALUE set_reconnect(VALUE self, VALUE value) { static VALUE set_connect_timeout(VALUE self, VALUE value) { unsigned int connect_timeout = 0; - GET_CLIENT(self) + GET_CLIENT(self); if(!NIL_P(value)) { connect_timeout = NUM2INT(value); @@ -453,7 +453,7 @@ static VALUE set_connect_timeout(VALUE self, VALUE value) { static VALUE set_charset_name(VALUE self, VALUE value) { char * charset_name; - GET_CLIENT(self) + GET_CLIENT(self); #ifdef HAVE_RUBY_ENCODING_H VALUE new_encoding; @@ -478,7 +478,7 @@ static VALUE set_charset_name(VALUE self, VALUE value) { } static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE capath, VALUE cipher) { - GET_CLIENT(self) + GET_CLIENT(self); if(!NIL_P(ca) || !NIL_P(key)) { mysql_ssl_set(wrapper->client, @@ -493,7 +493,7 @@ static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE } static VALUE init_connection(VALUE self) { - GET_CLIENT(self) + GET_CLIENT(self); if (rb_thread_blocking_region(nogvl_init, ((void *) &wrapper->client), RUBY_UBF_IO, 0) == Qfalse) { /* TODO: warning - not enough memory? */ From 5cadce3417a09335a220f5a58b1e6412dc76298b Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 14 Sep 2010 20:51:21 -0700 Subject: [PATCH 17/23] fix bug in rb_mysql_client_free --- ext/mysql2/client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 4d437b7..44e87f3 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -105,7 +105,7 @@ static VALUE nogvl_connect(void *ptr) { } static void rb_mysql_client_free(void * ptr) { - GET_CLIENT(ptr); + mysql_client_wrapper *wrapper = (mysql_client_wrapper *)ptr; /* * we'll send a QUIT message to the server, but that message is more of a From e7924df06ace8937b3d4f42728f92d1bf4032990 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 14 Sep 2010 22:27:07 -0700 Subject: [PATCH 18/23] make sure we only attempt to close/free the MYSQL pointer once --- ext/mysql2/client.c | 33 +++++++++++++++++++-------------- ext/mysql2/client.h | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 44e87f3..395728b 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -8,8 +8,8 @@ static VALUE intern_encoding_from_charset; static ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array; static ID intern_merge, intern_error_number_eql, intern_sql_state_eql; -#define REQUIRE_OPEN_DB(_ctxt) \ - if(!_ctxt->net.vio) { \ +#define REQUIRE_OPEN_DB(wrapper) \ + if(wrapper->closed || !wrapper->client->net.vio) { \ rb_raise(cMysql2Error, "closed MySQL connection"); \ return Qnil; \ } @@ -130,14 +130,18 @@ static void rb_mysql_client_free(void * ptr) { } /* It's safe to call mysql_close() on an already closed connection. */ - mysql_close(wrapper->client); + if (!wrapper->closed) { + mysql_close(wrapper->client); + } xfree(ptr); } static VALUE nogvl_close(void * ptr) { - MYSQL *client = (MYSQL *)ptr; - mysql_close(client); - client->net.fd = -1; + mysql_client_wrapper *wrapper = ptr; + if (!wrapper->closed) { + mysql_close(wrapper->client); + wrapper->closed = 1; + } return Qnil; } @@ -147,6 +151,7 @@ static VALUE allocate(VALUE klass) { obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper); wrapper->encoding = Qnil; wrapper->active = 0; + wrapper->closed = 0; return obj; } @@ -180,7 +185,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po static VALUE rb_mysql_client_close(VALUE self) { GET_CLIENT(self); - rb_thread_blocking_region(nogvl_close, wrapper->client, RUBY_UBF_IO, 0); + rb_thread_blocking_region(nogvl_close, wrapper, RUBY_UBF_IO, 0); return Qnil; } @@ -223,7 +228,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) { MYSQL_RES * result; GET_CLIENT(self); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); if (rb_thread_blocking_region(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { // an error occurred, mark this connection inactive MARK_CONN_INACTIVE(self); @@ -262,7 +267,7 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { VALUE opts, defaults; GET_CLIENT(self); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); args.mysql = wrapper->client; // see if this connection is still waiting on a result from a previous query @@ -340,7 +345,7 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { oldLen = RSTRING_LEN(str); newStr = rb_str_new(0, oldLen*2+1); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); newLen = mysql_real_escape_string(wrapper->client, RSTRING_PTR(newStr), StringValuePtr(str), oldLen); if (newLen == oldLen) { // no need to return a new ruby string if nothing changed @@ -385,7 +390,7 @@ static VALUE rb_mysql_client_server_info(VALUE self) { rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); #endif - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); version = rb_hash_new(); rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(wrapper->client))); @@ -402,19 +407,19 @@ static VALUE rb_mysql_client_server_info(VALUE self) { static VALUE rb_mysql_client_socket(VALUE self) { GET_CLIENT(self); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); return INT2NUM(wrapper->client->net.fd); } static VALUE rb_mysql_client_last_id(VALUE self) { GET_CLIENT(self); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); return ULL2NUM(mysql_insert_id(wrapper->client)); } static VALUE rb_mysql_client_affected_rows(VALUE self) { GET_CLIENT(self); - REQUIRE_OPEN_DB(wrapper->client); + REQUIRE_OPEN_DB(wrapper); return ULL2NUM(mysql_affected_rows(wrapper->client)); } diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h index 781ccda..d5c2993 100644 --- a/ext/mysql2/client.h +++ b/ext/mysql2/client.h @@ -34,6 +34,7 @@ void init_mysql2_client(); typedef struct { VALUE encoding; short int active; + short int closed; MYSQL *client; } mysql_client_wrapper; From dc9164d0167d15579d34f9fe13ec5ce9ac0d7526 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 14 Sep 2010 22:27:18 -0700 Subject: [PATCH 19/23] re-fix some compiler warnings --- ext/mysql2/result.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index a78c9e3..7375df5 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -95,7 +95,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo mysql2_result_wrapper * wrapper; MYSQL_ROW row; MYSQL_FIELD * fields = NULL; - long i = 0; + unsigned int i = 0; unsigned long * fieldLengths; void * ptr; @@ -286,7 +286,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo static VALUE rb_mysql_result_fetch_fields(VALUE self) { mysql2_result_wrapper * wrapper; - long i = 0; + unsigned int i = 0; short int symbolizeKeys = 0; VALUE defaults; From 26650c8c0554210c07968cc373f4ebec4c97e52f Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 17 Sep 2010 10:13:10 -0700 Subject: [PATCH 20/23] Version bump to 0.2.4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7179039..abd4105 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.3 +0.2.4 From 50775163edd1dbe9160e3acfa0e25c8d59ad844f Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 17 Sep 2010 10:19:16 -0700 Subject: [PATCH 21/23] prepare for 0.2.4 release --- CHANGELOG.md | 9 +++++++++ lib/mysql2.rb | 2 +- mysql2.gemspec | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f413d2..63d3e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.2.4 (September 17th, 2010) +* a few patches for win32 support from Luis Lavena - thanks man! +* bugfix from Eric Wong to avoid a potential stack overflow during Mysql2::Client#escape +* added the ability to turn internal row caching on/off via the :cache_rows => true/false option +* a couple of small patches for rbx compatibility +* set IndexDefinition#length in AR adapter - Kouhei Yanagita +* fix a long-standing data corruption bug - thank you thank you thank you to @joedamato (http://github.com/ice799) +* bugfix from calling mysql_close on a closed/freed connection surfaced by the above fix + ## 0.2.3 (August 20th, 2010) * connection flags can now be passed to the constructor via the :flags key * switch AR adapter connection over to use FOUND_ROWS option diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 52ae5a5..b6584de 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -12,5 +12,5 @@ require 'mysql2/result' # # A modern, simple and very fast Mysql library for Ruby - binding to libmysql module Mysql2 - VERSION = "0.2.3" + VERSION = "0.2.4" end diff --git a/mysql2.gemspec b/mysql2.gemspec index e49757f..75a9b8a 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{mysql2} - s.version = "0.2.3" + s.version = "0.2.4" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Brian Lopez"] - s.date = %q{2010-08-20} + s.date = %q{2010-09-17} s.email = %q{seniorlopez@gmail.com} s.extensions = ["ext/mysql2/extconf.rb"] s.extra_rdoc_files = [ From 84dc998a3be87d02b0bfe0079a7320b4016d238d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 18 Sep 2010 13:36:39 -0700 Subject: [PATCH 22/23] adding a task for fat binary shim --- .gitignore | 1 + tasks/compile.rake | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 07fb61a..1bc4656 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ mkmf.log pkg/ tmp vendor +lib/mysql2/mysql2.rb diff --git a/tasks/compile.rake b/tasks/compile.rake index 7979ae2..c61b7b9 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -30,3 +30,12 @@ Rake::ExtensionTask.new("mysql2", gemspec) do |ext| CLEAN.include "#{ext.lib_dir}/*.#{RbConfig::CONFIG['DLEXT']}" end Rake::Task[:spec].prerequisites << :compile + +file 'lib/mysql2/mysql2.rb' do + name = gemspec.name + File.open("lib/#{name}/#{name}.rb", 'wb') do |f| + f.write <<-eoruby +require "#{name}/\#{RUBY_VERSION.sub(/\\.\\d+$/, '')}/#{name}" + eoruby + end +end From ba41a5a24e51d622e388f4585792b4ab529f1268 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 18 Sep 2010 23:50:05 -0700 Subject: [PATCH 23/23] final rake task changes for fat binary support on windows --- tasks/compile.rake | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tasks/compile.rake b/tasks/compile.rake index c61b7b9..4dae1b2 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -31,6 +31,15 @@ Rake::ExtensionTask.new("mysql2", gemspec) do |ext| end Rake::Task[:spec].prerequisites << :compile +namespace :cross do + task :file_list do + gemspec.extensions = [] + gemspec.files += Dir["lib/#{gemspec.name}/#{gemspec.name}.rb"] + gemspec.files += Dir["lib/#{gemspec.name}/1.{8,9}/#{gemspec.name}.so"] + # gemspec.files += Dir["ext/mysql2/*.dll"] + end +end + file 'lib/mysql2/mysql2.rb' do name = gemspec.name File.open("lib/#{name}/#{name}.rb", 'wb') do |f| @@ -39,3 +48,8 @@ require "#{name}/\#{RUBY_VERSION.sub(/\\.\\d+$/, '')}/#{name}" eoruby end end + +if Rake::Task.task_defined?(:cross) + Rake::Task[:cross].prerequisites << "lib/mysql2/mysql2.rb" + Rake::Task[:cross].prerequisites << "cross:file_list" +end