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/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/README.rdoc b/README.rdoc index 9398586..a8df4a5 100644 --- a/README.rdoc +++ b/README.rdoc @@ -89,18 +89,18 @@ 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 +=== 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/VERSION b/VERSION index 7179039..abd4105 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.3 +0.2.4 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 diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 8181625..5bb0fee 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -8,20 +8,18 @@ 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; \ } #define MARK_CONN_INACTIVE(conn) \ - wrapper->active = 0; + wrapper->active = 0 #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,36 +105,43 @@ 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; + mysql_client_wrapper *wrapper = (mysql_client_wrapper *)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 flags; + int fd = wrapper->client->net.fd; 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. */ - mysql_close(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,12 +151,13 @@ 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; } 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); @@ -160,12 +165,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; @@ -178,9 +183,9 @@ 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, client, RUBY_UBF_IO, 0); + rb_thread_blocking_region(nogvl_close, wrapper, RUBY_UBF_IO, 0); return Qnil; } @@ -221,30 +226,30 @@ 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(client); - if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { + 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); - 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; } 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; @@ -260,10 +265,10 @@ 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(client); - args.mysql = client; + REQUIRE_OPEN_DB(wrapper); + args.mysql = wrapper->client; // see if this connection is still waiting on a result from a previous query if (wrapper->active == 0) { @@ -294,13 +299,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); @@ -327,7 +332,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 @@ -338,15 +343,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); + 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 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) { @@ -379,17 +384,17 @@ 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); #endif - REQUIRE_OPEN_DB(client); + REQUIRE_OPEN_DB(wrapper); 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) { @@ -401,34 +406,34 @@ 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); + GET_CLIENT(self); + 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(client); - return ULL2NUM(mysql_insert_id(client)); + GET_CLIENT(self); + 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(client); - return ULL2NUM(mysql_affected_rows(client)); + GET_CLIENT(self); + REQUIRE_OPEN_DB(wrapper); + 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; /* 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; @@ -436,16 +441,16 @@ 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); 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; @@ -453,7 +458,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; @@ -469,19 +474,19 @@ 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; } 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(client, + mysql_ssl_set(wrapper->client, NIL_P(key) ? NULL : StringValuePtr(key), NIL_P(cert) ? NULL : StringValuePtr(cert), NIL_P(ca) ? NULL : StringValuePtr(ca), @@ -493,11 +498,11 @@ 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, 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..d5c2993 100644 --- a/ext/mysql2/client.h +++ b/ext/mysql2/client.h @@ -34,7 +34,8 @@ void init_mysql2_client(); typedef struct { VALUE encoding; short int active; - MYSQL client; + short int closed; + MYSQL *client; } mysql_client_wrapper; #endif \ No newline at end of file diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 5c7302e..7375df5 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,11 +459,12 @@ 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"); - 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); 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/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 diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 934b92e..b52fb84 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -13,5 +13,5 @@ require 'mysql2/field' # # 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/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/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/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 = [ diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index b3ffcbc..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 @@ -144,6 +146,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 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 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 diff --git a/tasks/compile.rake b/tasks/compile.rake index 712eb17..4dae1b2 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -1,10 +1,15 @@ 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| + +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,8 +17,39 @@ 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' + + # clean compiled extension + CLEAN.include "#{ext.lib_dir}/*.#{RbConfig::CONFIG['DLEXT']}" 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| + f.write <<-eoruby +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