From 94ae2a781df133c2bfe69879c05554dfe7c02ccf Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 3 Aug 2010 19:04:30 -0700 Subject: [PATCH 01/20] no need to carry over options twice as we're already doing it up in rb_mysql_client_async_result --- ext/mysql2/client.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index dd99784..9573e8f 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -290,9 +290,6 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { VALUE result = rb_mysql_client_async_result(self); - // pass-through query options for result construction later - rb_iv_set(result, "@query_options", rb_obj_dup(opts)); - return result; } else { return Qnil; From 3ff7baa5f827efcbfd9434e27b4251f7c759e2ac Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 3 Aug 2010 20:21:51 -0700 Subject: [PATCH 02/20] libmysql only allows one query be sent at a time per connection, bail early if that's attempted --- ext/mysql2/client.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 9573e8f..f7783cd 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -225,6 +225,9 @@ static VALUE rb_mysql_client_async_result(VALUE self) { return Qnil; } + // we have a result, mark this connection inactive + rb_iv_set(self, "@active", Qfalse); + 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"))); @@ -240,13 +243,22 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { fd_set fdset; int fd, retval; int async = 0; - VALUE opts, defaults; + VALUE opts, defaults, active; MYSQL *client; Data_Get_Struct(self, MYSQL, client); REQUIRE_OPEN_DB(client); args.mysql = client; + active = rb_iv_get(self, "@active"); + // see if this connection is still waiting on a result from a previous query + if (NIL_P(active) || active == Qfalse) { + // mark this connection active + rb_iv_set(self, "@active", Qtrue); + } else { + rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result"); + } + defaults = rb_iv_get(self, "@query_options"); if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { opts = rb_funcall(defaults, intern_merge, 1, opts); From 1a70e83a746dc827c66d3cb9de649c899012ba3d Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 3 Aug 2010 20:22:44 -0700 Subject: [PATCH 03/20] Revert "libmysql only allows one query be sent at a time per connection, bail early if that's attempted" This reverts commit 3ff7baa5f827efcbfd9434e27b4251f7c759e2ac. --- ext/mysql2/client.c | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index f7783cd..9573e8f 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -225,9 +225,6 @@ static VALUE rb_mysql_client_async_result(VALUE self) { return Qnil; } - // we have a result, mark this connection inactive - rb_iv_set(self, "@active", Qfalse); - 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"))); @@ -243,22 +240,13 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { fd_set fdset; int fd, retval; int async = 0; - VALUE opts, defaults, active; + VALUE opts, defaults; MYSQL *client; Data_Get_Struct(self, MYSQL, client); REQUIRE_OPEN_DB(client); args.mysql = client; - active = rb_iv_get(self, "@active"); - // see if this connection is still waiting on a result from a previous query - if (NIL_P(active) || active == Qfalse) { - // mark this connection active - rb_iv_set(self, "@active", Qtrue); - } else { - rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result"); - } - defaults = rb_iv_get(self, "@query_options"); if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { opts = rb_funcall(defaults, intern_merge, 1, opts); From c0cf2f13a078ccacc763f3bb4b459846f516f77f Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 3 Aug 2010 20:37:49 -0700 Subject: [PATCH 04/20] let's try that again - libmysql only allows one query be sent at a time per connection, bail early if that's attempted --- ext/mysql2/client.c | 25 ++++++++++++++++++++++--- spec/mysql2/client_spec.rb | 7 +++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 9573e8f..abda82e 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -12,6 +12,9 @@ extern ID intern_merge; return Qnil; \ } +#define MARK_CONN_INACTIVE(conn) \ + rb_iv_set(conn, "@active", Qfalse); + /* * used to pass all arguments to mysql_real_connect while inside * rb_thread_blocking_region @@ -147,8 +150,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po args.mysql = client; args.client_flag = 0; - if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) - { + if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) { // unable to connect return rb_raise_mysql2_error(client); } @@ -214,10 +216,16 @@ static VALUE rb_mysql_client_async_result(VALUE self) { REQUIRE_OPEN_DB(client); if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { + // an error occurred, mark this connection inactive + MARK_CONN_INACTIVE(self); return rb_raise_mysql2_error(client); } result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, 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); @@ -240,13 +248,22 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { fd_set fdset; int fd, retval; int async = 0; - VALUE opts, defaults; + VALUE opts, defaults, active; MYSQL *client; Data_Get_Struct(self, MYSQL, client); REQUIRE_OPEN_DB(client); args.mysql = client; + active = rb_iv_get(self, "@active"); + // see if this connection is still waiting on a result from a previous query + if (NIL_P(active) || active == Qfalse) { + // mark this connection active + rb_iv_set(self, "@active", Qtrue); + } else { + rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result"); + } + defaults = rb_iv_get(self, "@query_options"); if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { opts = rb_funcall(defaults, intern_merge, 1, opts); @@ -266,6 +283,8 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { #endif 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); } diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 262dc6f..a6ff3c7 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -71,6 +71,13 @@ describe Mysql2::Client do it "should be able to return results with symbolized keys" do @client.query("SELECT 1", :symbolize_keys => true).first.keys[0].class.should eql(Symbol) end + + it "should not allow another query to be sent without fetching a result first" do + @client.query("SELECT 1", :async => true) + lambda { + @client.query("SELECT 1") + }.should raise_error(Mysql2::Error) + end end it "should respond to #escape" do From 923393351afe75da438a75152103ca73b46608d7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 3 Aug 2010 20:45:00 -0700 Subject: [PATCH 05/20] initialize @active early on to prevent warnings later --- lib/mysql2/client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index f2db76e..f4e3342 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -10,6 +10,7 @@ module Mysql2 def initialize(opts = {}) @query_options = @@default_query_options.dup + @active = false init_connection From 2bb8721e84e2b9b06d15e0d02cf35d92c371be03 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 4 Aug 2010 00:54:05 -0700 Subject: [PATCH 06/20] respect :symbolize_keys option for Mysql2::Result#fields if it's called before the first row is built --- ext/mysql2/result.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 02f70f0..0801030 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -237,9 +237,16 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKey static VALUE rb_mysql_result_fetch_fields(VALUE self) { mysql2_result_wrapper * wrapper; unsigned int i = 0; + short int symbolizeKeys = 0; + VALUE defaults; GetMysql2Result(self, wrapper); + defaults = rb_iv_get(self, "@query_options"); + if (rb_hash_aref(defaults, sym_symbolize_keys) == Qtrue) { + symbolizeKeys = 1; + } + if (wrapper->fields == Qnil) { wrapper->numberOfFields = mysql_num_fields(wrapper->result); wrapper->fields = rb_ary_new2(wrapper->numberOfFields); @@ -247,7 +254,7 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { if (RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) { for (i=0; inumberOfFields; i++) { - rb_mysql_result_fetch_field(self, i, 0); + rb_mysql_result_fetch_field(self, i, symbolizeKeys); } } From 12c022c8aa5ab4a2aead09af918a06e84777a730 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 4 Aug 2010 19:32:14 -0700 Subject: [PATCH 07/20] move most previously global symbols to static to prevent conflicts (thanks for catching this Eric) --- ext/mysql2/client.c | 18 +++++++++++++++--- ext/mysql2/mysql2_ext.c | 19 +------------------ ext/mysql2/result.c | 22 ++++++++++++++++------ 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index abda82e..3a06ed1 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -2,9 +2,10 @@ #include VALUE cMysql2Client; -extern VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; -extern ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array; -extern ID intern_merge; +extern VALUE mMysql2, cMysql2Error; +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; #define REQUIRE_OPEN_DB(_ctxt) \ if(!_ctxt->net.vio) { \ @@ -529,4 +530,15 @@ void init_mysql2_client() { rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5); rb_define_private_method(cMysql2Client, "init_connection", init_connection, 0); rb_define_private_method(cMysql2Client, "connect", rb_connect, 6); + + intern_encoding_from_charset = rb_intern("encoding_from_charset"); + + sym_id = ID2SYM(rb_intern("id")); + sym_version = ID2SYM(rb_intern("version")); + sym_async = ID2SYM(rb_intern("async")); + sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); + sym_as = ID2SYM(rb_intern("as")); + sym_array = ID2SYM(rb_intern("array")); + + intern_merge = rb_intern("merge"); } \ No newline at end of file diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index b027f45..dcb72f3 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -1,29 +1,12 @@ #include -VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; -ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, - sym_array, sym_timezone, sym_utc, sym_local; -ID intern_merge; +VALUE mMysql2, cMysql2Error; /* Ruby Extension initializer */ void Init_mysql2() { mMysql2 = rb_define_module("Mysql2"); cMysql2Error = rb_const_get(mMysql2, rb_intern("Error")); - intern_merge = rb_intern("merge"); - - sym_timezone = ID2SYM(rb_intern("timezone")); - sym_utc = ID2SYM(rb_intern("utc")); - sym_local = ID2SYM(rb_intern("local")); - sym_array = ID2SYM(rb_intern("array")); - sym_as = ID2SYM(rb_intern("as")); - sym_id = ID2SYM(rb_intern("id")); - sym_version = ID2SYM(rb_intern("version")); - sym_async = ID2SYM(rb_intern("async")); - sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); - - intern_encoding_from_charset = rb_intern("encoding_from_charset"); - init_mysql2_client(); init_mysql2_result(); } diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 0801030..b0880c6 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -4,13 +4,13 @@ rb_encoding *binaryEncoding; #endif -ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code; - VALUE cMysql2Result; VALUE cBigDecimal, cDate, cDateTime; -extern VALUE mMysql2, cMysql2Client, cMysql2Error, intern_encoding_from_charset; -extern ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc; -extern ID intern_merge; +extern VALUE mMysql2, cMysql2Client, cMysql2Error; +static VALUE intern_encoding_from_charset; +static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code; +static ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc; +static ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { mysql2_result_wrapper * w = wrapper; @@ -367,10 +367,20 @@ void init_mysql2_result() { rb_define_method(cMysql2Result, "each", rb_mysql_result_each, -1); rb_define_method(cMysql2Result, "fields", rb_mysql_result_fetch_fields, 0); + intern_encoding_from_charset = rb_intern("encoding_from_charset"); + intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code"); + intern_new = rb_intern("new"); intern_utc = rb_intern("utc"); intern_local = rb_intern("local"); - intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code"); + intern_merge = rb_intern("merge"); + + sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); + sym_as = ID2SYM(rb_intern("as")); + sym_array = ID2SYM(rb_intern("array")); + sym_timezone = ID2SYM(rb_intern("timezone")); + sym_local = ID2SYM(rb_intern("local")); + sym_utc = ID2SYM(rb_intern("utc")); #ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); From 2514fafa536327ee5ed6f3dbc7de589ffb7d4fbe Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 00:39:11 -0700 Subject: [PATCH 08/20] add :cast_booleans option for automatically casting tinyint(1) fields into true/false for ruby --- ext/mysql2/result.c | 18 +++++++--- lib/mysql2/client.rb | 3 +- spec/mysql2/result_spec.rb | 70 ++++++++------------------------------ spec/spec_helper.rb | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 61 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index b0880c6..f61195f 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -9,7 +9,8 @@ VALUE cBigDecimal, cDate, cDateTime; extern VALUE mMysql2, cMysql2Client, cMysql2Error; static VALUE intern_encoding_from_charset; static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code; -static ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc; +static ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc, + sym_cast_booleans; static ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { @@ -86,7 +87,7 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int return rb_field; } -static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKeys, int asArray) { +static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKeys, int asArray, int castBool) { VALUE rowVal; mysql2_result_wrapper * wrapper; MYSQL_ROW row; @@ -131,6 +132,10 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKey val = rb_str_new(row[i], fieldLengths[i]); break; case MYSQL_TYPE_TINY: // TINYINT field + if (castBool && fields[i].length == 1) { + val = *row[i] == '1' ? Qtrue : Qfalse; + break; + } case MYSQL_TYPE_SHORT: // SMALLINT field case MYSQL_TYPE_LONG: // INTEGER field case MYSQL_TYPE_INT24: // MEDIUMINT field @@ -266,7 +271,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { ID timezone; mysql2_result_wrapper * wrapper; unsigned long i; - int symbolizeKeys = 0, asArray = 0; + int symbolizeKeys = 0, asArray = 0, castBool = 0; GetMysql2Result(self, wrapper); @@ -285,6 +290,10 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { asArray = 1; } + if (rb_hash_aref(opts, sym_cast_booleans) == Qtrue) { + castBool = 1; + } + timezoneVal = rb_hash_aref(opts, sym_timezone); if (timezoneVal == sym_local) { timezone = intern_local; @@ -318,7 +327,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { if (i < rowsProcessed) { row = rb_ary_entry(wrapper->rows, i); } else { - row = rb_mysql_result_fetch_row(self, timezone, symbolizeKeys, asArray); + row = rb_mysql_result_fetch_row(self, timezone, symbolizeKeys, asArray, castBool); rb_ary_store(wrapper->rows, i, row); wrapper->lastRowProcessed++; } @@ -381,6 +390,7 @@ void init_mysql2_result() { sym_timezone = ID2SYM(rb_intern("timezone")); sym_local = ID2SYM(rb_intern("local")); sym_utc = ID2SYM(rb_intern("utc")); + sym_cast_booleans = ID2SYM(rb_intern("cast_booleans")); #ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index f4e3342..344313a 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -5,7 +5,8 @@ module Mysql2 :symbolize_keys => false, :async => false, :as => :hash, - :timezone => :local + :timezone => :local, + :cast_booleans => false } def initialize(opts = {}) diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index 8585656..fd230cb 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -71,65 +71,9 @@ describe Mysql2::Result do context "row data type mapping" do before(:each) do @client.query "USE test" - @client.query %[ - CREATE TABLE IF NOT EXISTS mysql2_test ( - id MEDIUMINT NOT NULL AUTO_INCREMENT, - null_test VARCHAR(10), - bit_test BIT(64), - tiny_int_test TINYINT, - small_int_test SMALLINT, - medium_int_test MEDIUMINT, - int_test INT, - big_int_test BIGINT, - float_test FLOAT(10,3), - double_test DOUBLE(10,3), - decimal_test DECIMAL(10,3), - date_test DATE, - date_time_test DATETIME, - timestamp_test TIMESTAMP, - time_test TIME, - year_test YEAR(4), - char_test CHAR(10), - varchar_test VARCHAR(10), - binary_test BINARY(10), - varbinary_test VARBINARY(10), - tiny_blob_test TINYBLOB, - tiny_text_test TINYTEXT, - blob_test BLOB, - text_test TEXT, - medium_blob_test MEDIUMBLOB, - medium_text_test MEDIUMTEXT, - long_blob_test LONGBLOB, - long_text_test LONGTEXT, - enum_test ENUM('val1', 'val2'), - set_test SET('val1', 'val2'), - PRIMARY KEY (id) - ) - ] - @client.query %[ - INSERT INTO mysql2_test ( - null_test, bit_test, tiny_int_test, small_int_test, medium_int_test, int_test, big_int_test, - float_test, double_test, decimal_test, date_test, date_time_test, timestamp_test, time_test, - year_test, char_test, varchar_test, binary_test, varbinary_test, tiny_blob_test, - tiny_text_test, blob_test, text_test, medium_blob_test, medium_text_test, - long_blob_test, long_text_test, enum_test, set_test - ) - - VALUES ( - NULL, b'101', 1, 10, 10, 10, 10, - 10.3, 10.3, 10.3, '2010-4-4', '2010-4-4 11:44:00', '2010-4-4 11:44:00', '11:44:00', - 2009, "test", "test", "test", "test", "test", - "test", "test", "test", "test", "test", - "test", "test", 'val1', 'val1,val2' - ) - ] @test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first end - after(:all) do - @client.query("DELETE FROM mysql2_test WHERE id=#{@test_result['id']}") - end - it "should return nil for a NULL value" do @test_result['null_test'].class.should eql(NilClass) @test_result['null_test'].should eql(nil) @@ -145,6 +89,20 @@ describe Mysql2::Result do @test_result['tiny_int_test'].should eql(1) end + it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do + @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (1)' + id1 = @client.last_id + @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (0)' + id2 = @client.last_id + + result1 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast_booleans => true + result2 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 0 LIMIT 1', :cast_booleans => true + result1.first['bool_cast_test'].should be_true + result2.first['bool_cast_test'].should be_false + + @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})" + end + it "should return Fixnum for a SMALLINT value" do [Fixnum, Bignum].should include(@test_result['small_int_test'].class) @test_result['small_int_test'].should eql(10) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d942a6..b50e6cc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,3 +3,62 @@ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/..') $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'mysql2' + +Spec::Runner.configure do |config| + config.before(:all) do + client = Mysql2::Client.new :database => 'test' + client.query %[ + CREATE TABLE IF NOT EXISTS mysql2_test ( + id MEDIUMINT NOT NULL AUTO_INCREMENT, + null_test VARCHAR(10), + bit_test BIT(64), + tiny_int_test TINYINT, + bool_cast_test TINYINT(1), + small_int_test SMALLINT, + medium_int_test MEDIUMINT, + int_test INT, + big_int_test BIGINT, + float_test FLOAT(10,3), + double_test DOUBLE(10,3), + decimal_test DECIMAL(10,3), + date_test DATE, + date_time_test DATETIME, + timestamp_test TIMESTAMP, + time_test TIME, + year_test YEAR(4), + char_test CHAR(10), + varchar_test VARCHAR(10), + binary_test BINARY(10), + varbinary_test VARBINARY(10), + tiny_blob_test TINYBLOB, + tiny_text_test TINYTEXT, + blob_test BLOB, + text_test TEXT, + medium_blob_test MEDIUMBLOB, + medium_text_test MEDIUMTEXT, + long_blob_test LONGBLOB, + long_text_test LONGTEXT, + enum_test ENUM('val1', 'val2'), + set_test SET('val1', 'val2'), + PRIMARY KEY (id) + ) + ] + client.query %[ + INSERT INTO mysql2_test ( + null_test, bit_test, tiny_int_test, bool_cast_test, small_int_test, medium_int_test, int_test, big_int_test, + float_test, double_test, decimal_test, date_test, date_time_test, timestamp_test, time_test, + year_test, char_test, varchar_test, binary_test, varbinary_test, tiny_blob_test, + tiny_text_test, blob_test, text_test, medium_blob_test, medium_text_test, + long_blob_test, long_text_test, enum_test, set_test + ) + + VALUES ( + NULL, b'101', 1, 1, 10, 10, 10, 10, + 10.3, 10.3, 10.3, '2010-4-4', '2010-4-4 11:44:00', '2010-4-4 11:44:00', '11:44:00', + 2009, "test", "test", "test", "test", "test", + "test", "test", "test", "test", "test", + "test", "test", 'val1', 'val1,val2' + ) + ] + end +end \ No newline at end of file From d1b2f98b7cca7bfff4ceabf4b7ff11f6fc4de886 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 00:44:01 -0700 Subject: [PATCH 09/20] major refactor of Sequel adapter - it's now green in Sequel --- lib/sequel/adapters/mysql2.rb | 116 ++++++++++++++-------------------- 1 file changed, 48 insertions(+), 68 deletions(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 26b74bf..5fb443e 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -1,42 +1,29 @@ require 'mysql2' unless defined? Mysql2 -Sequel.require %w'shared/mysql utils/stored_procedures', 'adapters' +Sequel.require %w'shared/mysql', 'adapters' module Sequel - # Module for holding all MySQL-related classes and modules for Sequel. + # Module for holding all Mysql2-related classes and modules for Sequel. module Mysql2 - # Mapping of type numbers to conversion procs - MYSQL_TYPES = {} + class << self + # Set the default charset used for CREATE TABLE. You can pass the + # :charset option to create_table to override this setting. + attr_accessor :default_charset - MYSQL2_LITERAL_PROC = lambda{|v| v} + # Set the default collation used for CREATE TABLE. You can pass the + # :collate option to create_table to override this setting. + attr_accessor :default_collate - # Use only a single proc for each type to save on memory - MYSQL_TYPE_PROCS = { - [0, 246] => MYSQL2_LITERAL_PROC, # decimal - [1] => lambda{|v| convert_tinyint_to_bool ? v != 0 : v}, # tinyint - [2, 3, 8, 9, 13, 247, 248] => MYSQL2_LITERAL_PROC, # integer - [4, 5] => MYSQL2_LITERAL_PROC, # float - [10, 14] => MYSQL2_LITERAL_PROC, # date - [7, 12] => MYSQL2_LITERAL_PROC, # datetime - [11] => MYSQL2_LITERAL_PROC, # time - [249, 250, 251, 252] => lambda{|v| Sequel::SQL::Blob.new(v)} # blob - } - MYSQL_TYPE_PROCS.each do |k,v| - k.each{|n| MYSQL_TYPES[n] = v} + # Set the default engine used for CREATE TABLE. You can pass the + # :engine option to create_table to override this setting. + attr_accessor :default_engine end - @convert_invalid_date_time = false @convert_tinyint_to_bool = true class << self - # By default, Sequel raises an exception if in invalid date or time is used. - # However, if this is set to nil or :nil, the adapter treats dates - # like 0000-00-00 and times like 838:00:00 as nil values. If set to :string, - # it returns the strings as is. - attr_accessor :convert_invalid_date_time - # Sequel converts the column type tinyint(1) to a boolean by default when - # using the native MySQL adapter. You can turn off the conversion by setting + # using the native Mysql2 adapter. You can turn off the conversion by setting # this to false. attr_accessor :convert_tinyint_to_bool end @@ -50,12 +37,6 @@ module Sequel set_adapter_scheme :mysql2 - # Support stored procedures on MySQL - def call_sproc(name, opts={}, &block) - args = opts[:args] || [] - execute("CALL #{name}#{args.empty? ? '()' : literal(args)}", opts.merge(:sproc=>false), &block) - end - # Connect to the database. In addition to the usual database options, # the following options have effect: # @@ -75,20 +56,25 @@ module Sequel # disconnect this connection. def connect(server) opts = server_opts(server) - conn = ::Mysql2::Client.new({ - :host => opts[:host] || 'localhost', - :username => opts[:user], - :password => opts[:password], - :database => opts[:database], - :port => opts[:port], - :socket => opts[:socket] - }) + opts[:host] ||= 'localhost' + conn = ::Mysql2::Client.new(opts) + + sqls = [] + # Set encoding a slightly different way after connecting, + # in case the READ_DEFAULT_GROUP overrode the provided encoding. + # Doesn't work across implicit reconnects, but Sequel doesn't turn on + # that feature. + if encoding = opts[:encoding] || opts[:charset] + sqls << "SET NAMES #{literal(encoding.to_s)}" + end # increase timeout so mysql server doesn't disconnect us - conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}") + sqls << "SET @@wait_timeout = #{opts[:timeout] || 2592000}" # By default, MySQL 'where id is null' selects the last inserted id - conn.query("set SQL_AUTO_IS_NULL=0") unless opts[:auto_is_null] + sqls << "SET SQL_AUTO_IS_NULL=0" unless opts[:auto_is_null] + + sqls.each{|sql| log_yield(sql){conn.query(sql)}} conn end @@ -110,17 +96,30 @@ module Sequel # Return the version of the MySQL server two which we are connecting. def server_version(server=nil) - @server_version ||= (synchronize(server){|conn| conn.info[:id]}) + @server_version ||= (synchronize(server){|conn| conn.server_info[:id]} || super) end private + # Use MySQL specific syntax for engine type and character encoding + def create_table_sql(name, generator, options = {}) + engine = options.fetch(:engine, Sequel::Mysql2.default_engine) + charset = options.fetch(:charset, Sequel::Mysql2.default_charset) + collate = options.fetch(:collate, Sequel::Mysql2.default_collate) + generator.columns.each do |c| + if t = c.delete(:table) + generator.foreign_key([c[:name]], t, c.merge(:name=>nil, :type=>:foreign_key)) + end + end + super(name, generator, options.merge(:engine => engine, :charset => charset, :collate => collate)) + end + # Execute the given SQL on the given connection. If the :type # option is :select, yield the result of the query, otherwise # yield the connection if a block is given. def _execute(conn, sql, opts) begin - r = log_yield(sql){conn.query(sql)} + r = log_yield(sql){conn.query(sql, :symbolize_keys => true)} if opts[:type] == :select yield r if r elsif block_given? @@ -136,7 +135,7 @@ module Sequel :query end - # The MySQL adapter main error class is Mysql::Error + # The MySQL adapter main error class is Mysql2::Error def database_error_classes [::Mysql2::Error] end @@ -161,36 +160,17 @@ module Sequel # Dataset class for MySQL datasets accessed via the native driver. class Dataset < Sequel::Dataset include Sequel::MySQL::DatasetMethods - include StoredProcedures - - # Methods for MySQL stored procedures using the native driver. - module StoredProcedureMethods - include Sequel::Dataset::StoredProcedureMethods - - private - - # Execute the database stored procedure with the stored arguments. - def execute(sql, opts={}, &block) - super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block) - end - - # Same as execute, explicit due to intricacies of alias and super. - def execute_dui(sql, opts={}, &block) - super(@sproc_name, {:args=>@sproc_args, :sproc=>true}.merge(opts), &block) - end - end # Delete rows matching this dataset def delete execute_dui(delete_sql){|c| return c.affected_rows} end - # Yield all rows matching this dataset. If the dataset is set to - # split multiple statements, yield arrays of hashes one per statement - # instead of yielding results for all statements as hashes. + # Yield all rows matching this dataset. def fetch_rows(sql, &block) execute(sql) do |r| - r.each(:symbolize_keys => true, &block) + @columns = r.fields + r.each(:cast_booleans => Sequel::Mysql2.convert_tinyint_to_bool, &block) end self end @@ -228,7 +208,7 @@ module Sequel super(sql, {:type=>:dui}.merge(opts), &block) end - # Handle correct quoting of strings using ::Mysql2#escape. + # Handle correct quoting of strings using ::MySQL.quote. def literal_string(v) db.synchronize{|c| "'#{c.escape(v)}'"} end From 86302eb1f1b8093934509827fe46f60246ec8a05 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 00:45:33 -0700 Subject: [PATCH 10/20] fix typo in comment --- lib/sequel/adapters/mysql2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 5fb443e..168bb52 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -208,7 +208,7 @@ module Sequel super(sql, {:type=>:dui}.merge(opts), &block) end - # Handle correct quoting of strings using ::MySQL.quote. + # Handle correct quoting of strings using ::Mysql2#escape. def literal_string(v) db.synchronize{|c| "'#{c.escape(v)}'"} end From 93fabe2b24558915a5fb94dda2810b9f0526f377 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 00:45:56 -0700 Subject: [PATCH 11/20] heh --- lib/sequel/adapters/mysql2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 168bb52..4da07d7 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -208,7 +208,7 @@ module Sequel super(sql, {:type=>:dui}.merge(opts), &block) end - # Handle correct quoting of strings using ::Mysql2#escape. + # Handle correct quoting of strings using ::Mysql2::Client#escape. def literal_string(v) db.synchronize{|c| "'#{c.escape(v)}'"} end From 04932b549bd48874ca6b78cb0db0809dbb8b89e3 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 01:20:15 -0700 Subject: [PATCH 12/20] Mysql2::Client uses the :username key, set it to :user if that was used instead --- lib/sequel/adapters/mysql2.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 4da07d7..b2187ce 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -57,6 +57,7 @@ module Sequel def connect(server) opts = server_opts(server) opts[:host] ||= 'localhost' + opts[:username] ||= opts[:user] conn = ::Mysql2::Client.new(opts) sqls = [] From 1bdf44ce7f38eeaa528d7618b472100f5bbeaf63 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 01:20:42 -0700 Subject: [PATCH 13/20] can't call literal here because it'll try to join it's own thread --- lib/sequel/adapters/mysql2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index b2187ce..2105cf9 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -66,7 +66,7 @@ module Sequel # Doesn't work across implicit reconnects, but Sequel doesn't turn on # that feature. if encoding = opts[:encoding] || opts[:charset] - sqls << "SET NAMES #{literal(encoding.to_s)}" + sqls << "SET NAMES #{conn.escape(encoding.to_s)}" end # increase timeout so mysql server doesn't disconnect us From ad34357e57e4d2752f1bca73c67b922f3a9cb744 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 22:50:45 -0700 Subject: [PATCH 14/20] convert :timezone option into two new ones :database_timezone - the timezone (:utc or :local) Mysql2 will assume time/datetime fields are stored in the db. This modifies what initial timezone your Time objects will be in when creating them from libmysql in C and :application_timezone - the timezone (:utc or :local) you'd finally like the Time objects converted to before you get them --- ext/mysql2/result.c | 73 +++++++++++++++++++++++++++++++------------- lib/mysql2/client.rb | 9 +++--- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index f61195f..b1850f4 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -8,9 +8,10 @@ VALUE cMysql2Result; VALUE cBigDecimal, cDate, cDateTime; extern VALUE mMysql2, cMysql2Client, cMysql2Error; static VALUE intern_encoding_from_charset; -static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code; -static ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc, - sym_cast_booleans; +static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code, + intern_localtime; +static ID sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_application_timezone, + sym_local, sym_utc, sym_cast_booleans; static ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { @@ -87,7 +88,7 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int return rb_field; } -static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKeys, int asArray, int castBool) { +static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezone, int symbolizeKeys, int asArray, int castBool) { VALUE rowVal; mysql2_result_wrapper * wrapper; MYSQL_ROW row; @@ -154,7 +155,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKey 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, timezone, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), 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); + } else { // utc + val = rb_funcall(val, intern_utc, 0); + } + } break; } case MYSQL_TYPE_TIMESTAMP: // TIMESTAMP field @@ -168,7 +176,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKey rb_raise(cMysql2Error, "Invalid date: %s", row[i]); val = Qnil; } else { - val = rb_funcall(rb_cTime, timezone, 6, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, db_timezone, 6, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + if (!NIL_P(app_timezone)) { + if (app_timezone == intern_local) { + val = rb_funcall(val, intern_localtime, 0); + } else { // utc + val = rb_funcall(val, intern_utc, 0); + } + } } } break; @@ -267,8 +282,8 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { } static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { - VALUE defaults, opts, block, timezoneVal; - ID timezone; + VALUE defaults, opts, block; + ID db_timezone, app_timezone, dbTz, appTz; mysql2_result_wrapper * wrapper; unsigned long i; int symbolizeKeys = 0, asArray = 0, castBool = 0; @@ -294,14 +309,28 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { castBool = 1; } - timezoneVal = rb_hash_aref(opts, sym_timezone); - if (timezoneVal == sym_local) { - timezone = intern_local; - } else if (timezoneVal == sym_utc) { - timezone = intern_utc; + dbTz = rb_hash_aref(opts, sym_database_timezone); + if (dbTz == sym_local) { + db_timezone = intern_local; + } else if (dbTz == sym_utc) { + db_timezone = intern_utc; } else { - rb_warn(":timezone config option must be :utc or :local - defaulting to :local"); - timezone = intern_local; + if (!NIL_P(dbTz)) { + rb_warn(":database_timezone option must be :utc or :local - defaulting to :local"); + } + db_timezone = intern_local; + } + + appTz = rb_hash_aref(opts, sym_application_timezone); + if (appTz == sym_local) { + app_timezone = intern_local; + } else if (appTz == sym_utc) { + app_timezone = intern_utc; + } else { + if (!NIL_P(appTz)) { + rb_warn(":application_timezone option must be :utc or :local - defaulting to :local"); + } + app_timezone = intern_local; } if (wrapper->lastRowProcessed == 0) { @@ -327,7 +356,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { if (i < rowsProcessed) { row = rb_ary_entry(wrapper->rows, i); } else { - row = rb_mysql_result_fetch_row(self, timezone, symbolizeKeys, asArray, castBool); + row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool); rb_ary_store(wrapper->rows, i, row); wrapper->lastRowProcessed++; } @@ -379,18 +408,20 @@ void init_mysql2_result() { intern_encoding_from_charset = rb_intern("encoding_from_charset"); intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code"); - intern_new = rb_intern("new"); - intern_utc = rb_intern("utc"); - intern_local = rb_intern("local"); - intern_merge = rb_intern("merge"); + intern_new = rb_intern("new"); + intern_utc = rb_intern("utc"); + intern_local = rb_intern("local"); + intern_merge = rb_intern("merge"); + intern_localtime = rb_intern("localtime"); sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); sym_as = ID2SYM(rb_intern("as")); sym_array = ID2SYM(rb_intern("array")); - sym_timezone = ID2SYM(rb_intern("timezone")); sym_local = ID2SYM(rb_intern("local")); sym_utc = ID2SYM(rb_intern("utc")); sym_cast_booleans = ID2SYM(rb_intern("cast_booleans")); + sym_database_timezone = ID2SYM(rb_intern("database_timezone")); + sym_application_timezone = ID2SYM(rb_intern("application_timezone")); #ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index 344313a..a830b5b 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -2,11 +2,12 @@ module Mysql2 class Client attr_reader :query_options @@default_query_options = { - :symbolize_keys => false, - :async => false, :as => :hash, - :timezone => :local, - :cast_booleans => false + :async => false, + :cast_booleans => false, + :symbolize_keys => false, + :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in + :application_timezone => :local # timezone Mysql2 will convert to before handing the object back to the caller } def initialize(opts = {}) From f4fb9e803406de5e2d13b52b13960f1af77b5450 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 22:53:36 -0700 Subject: [PATCH 15/20] sync up with sequel adapter from my Sequel fork until it's officially merged in --- lib/sequel/adapters/mysql2.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 2105cf9..89298b1 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -119,8 +119,11 @@ module Sequel # option is :select, yield the result of the query, otherwise # yield the connection if a block is given. def _execute(conn, sql, opts) + query_opts = {:symbolize_keys => true} + query_opts.merge!(:database_timezone => Sequel.database_timezone) if Sequel.respond_to?(:database_timezone) + query_opts.merge!(:application_timezone => Sequel.application_timezone) if Sequel.respond_to?(:application_timezone) begin - r = log_yield(sql){conn.query(sql, :symbolize_keys => true)} + r = log_yield(sql){conn.query(sql, query_opts)} if opts[:type] == :select yield r if r elsif block_given? From 957b0bac1bc0d7e7d8a4b0838918a70f2fac3705 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 5 Aug 2010 23:09:13 -0700 Subject: [PATCH 16/20] default application_timezone to nil --- lib/mysql2/client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index a830b5b..5206e54 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -6,8 +6,8 @@ module Mysql2 :async => false, :cast_booleans => false, :symbolize_keys => false, - :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in - :application_timezone => :local # timezone Mysql2 will convert to before handing the object back to the caller + :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 } def initialize(opts = {}) From 99af726a58eb0cd8923dc15d536dbb9c1af11e22 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 6 Aug 2010 00:01:09 -0700 Subject: [PATCH 17/20] application_timezone is allowed to be nil --- ext/mysql2/result.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index b1850f4..6baa678 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -327,9 +327,6 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { } else if (appTz == sym_utc) { app_timezone = intern_utc; } else { - if (!NIL_P(appTz)) { - rb_warn(":application_timezone option must be :utc or :local - defaulting to :local"); - } app_timezone = intern_local; } From 2527454b2619cbf31a75ed5fc08ae894765768f7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 6 Aug 2010 00:01:24 -0700 Subject: [PATCH 18/20] update AR adapter to reflect timezone setting update --- lib/active_record/connection_adapters/mysql2_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index d4345a4..5687597 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -302,7 +302,7 @@ module ActiveRecord def execute(sql, name = nil) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection - @connection.query_options[:timezone] = ActiveRecord::Base.default_timezone + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone if name == :skip_logging @connection.query(sql) else From b448b9814604cbc202fc70c4735e3eafc5784003 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 6 Aug 2010 11:19:34 -0700 Subject: [PATCH 19/20] move -Wextra to development flags area --- ext/mysql2/extconf.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mysql2/extconf.rb b/ext/mysql2/extconf.rb index 08f3803..bc48e6b 100644 --- a/ext/mysql2/extconf.rb +++ b/ext/mysql2/extconf.rb @@ -57,7 +57,7 @@ end asplode h unless have_header h end -$CFLAGS << ' -Wall -Wextra -funroll-loops' -# $CFLAGS << ' -O0 -ggdb3' +$CFLAGS << ' -Wall -funroll-loops' +# $CFLAGS << ' -O0 -ggdb3 -Wextra' create_makefile('mysql2/mysql2') From 7bf988910352e282f59f99b70091f048b4451dd7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 6 Aug 2010 12:47:25 -0700 Subject: [PATCH 20/20] remove Sequel adapter as it's now in Sequel core :) --- lib/sequel/adapters/mysql2.rb | 221 ---------------------------------- 1 file changed, 221 deletions(-) delete mode 100644 lib/sequel/adapters/mysql2.rb diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb deleted file mode 100644 index 89298b1..0000000 --- a/lib/sequel/adapters/mysql2.rb +++ /dev/null @@ -1,221 +0,0 @@ -require 'mysql2' unless defined? Mysql2 - -Sequel.require %w'shared/mysql', 'adapters' - -module Sequel - # Module for holding all Mysql2-related classes and modules for Sequel. - module Mysql2 - class << self - # Set the default charset used for CREATE TABLE. You can pass the - # :charset option to create_table to override this setting. - attr_accessor :default_charset - - # Set the default collation used for CREATE TABLE. You can pass the - # :collate option to create_table to override this setting. - attr_accessor :default_collate - - # Set the default engine used for CREATE TABLE. You can pass the - # :engine option to create_table to override this setting. - attr_accessor :default_engine - end - - @convert_tinyint_to_bool = true - - class << self - # Sequel converts the column type tinyint(1) to a boolean by default when - # using the native Mysql2 adapter. You can turn off the conversion by setting - # this to false. - attr_accessor :convert_tinyint_to_bool - end - - # Database class for MySQL databases used with Sequel. - class Database < Sequel::Database - include Sequel::MySQL::DatabaseMethods - - # Mysql::Error messages that indicate the current connection should be disconnected - MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away)/ - - set_adapter_scheme :mysql2 - - # Connect to the database. In addition to the usual database options, - # the following options have effect: - # - # * :auto_is_null - Set to true to use MySQL default behavior of having - # a filter for an autoincrement column equals NULL to return the last - # inserted row. - # * :charset - Same as :encoding (:encoding takes precendence) - # * :compress - Set to false to not compress results from the server - # * :config_default_group - The default group to read from the in - # the MySQL config file. - # * :config_local_infile - If provided, sets the Mysql::OPT_LOCAL_INFILE - # option on the connection with the given value. - # * :encoding - Set all the related character sets for this - # connection (connection, client, database, server, and results). - # * :socket - Use a unix socket file instead of connecting via TCP/IP. - # * :timeout - Set the timeout in seconds before the server will - # disconnect this connection. - def connect(server) - opts = server_opts(server) - opts[:host] ||= 'localhost' - opts[:username] ||= opts[:user] - conn = ::Mysql2::Client.new(opts) - - sqls = [] - # Set encoding a slightly different way after connecting, - # in case the READ_DEFAULT_GROUP overrode the provided encoding. - # Doesn't work across implicit reconnects, but Sequel doesn't turn on - # that feature. - if encoding = opts[:encoding] || opts[:charset] - sqls << "SET NAMES #{conn.escape(encoding.to_s)}" - end - - # increase timeout so mysql server doesn't disconnect us - sqls << "SET @@wait_timeout = #{opts[:timeout] || 2592000}" - - # By default, MySQL 'where id is null' selects the last inserted id - sqls << "SET SQL_AUTO_IS_NULL=0" unless opts[:auto_is_null] - - sqls.each{|sql| log_yield(sql){conn.query(sql)}} - - conn - end - - # Returns instance of Sequel::MySQL::Dataset with the given options. - def dataset(opts = nil) - Mysql2::Dataset.new(self, opts) - end - - # Executes the given SQL using an available connection, yielding the - # connection if the block is given. - def execute(sql, opts={}, &block) - if opts[:sproc] - call_sproc(sql, opts, &block) - else - synchronize(opts[:server]){|conn| _execute(conn, sql, opts, &block)} - end - end - - # Return the version of the MySQL server two which we are connecting. - def server_version(server=nil) - @server_version ||= (synchronize(server){|conn| conn.server_info[:id]} || super) - end - - private - - # Use MySQL specific syntax for engine type and character encoding - def create_table_sql(name, generator, options = {}) - engine = options.fetch(:engine, Sequel::Mysql2.default_engine) - charset = options.fetch(:charset, Sequel::Mysql2.default_charset) - collate = options.fetch(:collate, Sequel::Mysql2.default_collate) - generator.columns.each do |c| - if t = c.delete(:table) - generator.foreign_key([c[:name]], t, c.merge(:name=>nil, :type=>:foreign_key)) - end - end - super(name, generator, options.merge(:engine => engine, :charset => charset, :collate => collate)) - end - - # Execute the given SQL on the given connection. If the :type - # option is :select, yield the result of the query, otherwise - # yield the connection if a block is given. - def _execute(conn, sql, opts) - query_opts = {:symbolize_keys => true} - query_opts.merge!(:database_timezone => Sequel.database_timezone) if Sequel.respond_to?(:database_timezone) - query_opts.merge!(:application_timezone => Sequel.application_timezone) if Sequel.respond_to?(:application_timezone) - begin - r = log_yield(sql){conn.query(sql, query_opts)} - if opts[:type] == :select - yield r if r - elsif block_given? - yield conn - end - rescue ::Mysql2::Error => e - raise_error(e, :disconnect=>MYSQL_DATABASE_DISCONNECT_ERRORS.match(e.message)) - end - end - - # MySQL connections use the query method to execute SQL without a result - def connection_execute_method - :query - end - - # The MySQL adapter main error class is Mysql2::Error - def database_error_classes - [::Mysql2::Error] - end - - # The database name when using the native adapter is always stored in - # the :database option. - def database_name - @opts[:database] - end - - # Closes given database connection. - def disconnect_connection(c) - c.close - end - - # Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true - def schema_column_type(db_type) - Sequel::Mysql2.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super - end - end - - # Dataset class for MySQL datasets accessed via the native driver. - class Dataset < Sequel::Dataset - include Sequel::MySQL::DatasetMethods - - # Delete rows matching this dataset - def delete - execute_dui(delete_sql){|c| return c.affected_rows} - end - - # Yield all rows matching this dataset. - def fetch_rows(sql, &block) - execute(sql) do |r| - @columns = r.fields - r.each(:cast_booleans => Sequel::Mysql2.convert_tinyint_to_bool, &block) - end - self - end - - # Don't allow graphing a dataset that splits multiple statements - def graph(*) - raise(Error, "Can't graph a dataset that splits multiple result sets") if opts[:split_multiple_result_sets] - super - end - - # Insert a new value into this dataset - def insert(*values) - execute_dui(insert_sql(*values)){|c| return c.last_id} - end - - # Replace (update or insert) the matching row. - def replace(*args) - execute_dui(replace_sql(*args)){|c| return c.last_id} - end - - # Update the matching rows. - def update(values={}) - execute_dui(update_sql(values)){|c| return c.affected_rows} - end - - private - - # Set the :type option to :select if it hasn't been set. - def execute(sql, opts={}, &block) - super(sql, {:type=>:select}.merge(opts), &block) - end - - # Set the :type option to :dui if it hasn't been set. - def execute_dui(sql, opts={}, &block) - super(sql, {:type=>:dui}.merge(opts), &block) - end - - # Handle correct quoting of strings using ::Mysql2::Client#escape. - def literal_string(v) - db.synchronize{|c| "'#{c.escape(v)}'"} - end - end - end -end