From 36c243be7120eb8c1efe7a8b06df368d31f5c96e Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sun, 1 Aug 2010 20:20:48 -0700 Subject: [PATCH 01/10] Add cascading options hash at Mysql2::Client.default_query_options, which can be overridden by passing options to Mysql2::Client#query and/or Mysql2::Result#each Tune up specs, benchmarks and AR adapter to conform --- benchmark/query_with_mysql_casting.rb | 4 +- benchmark/query_without_mysql_casting.rb | 4 +- ext/mysql2/client.c | 43 +++++++++----- ext/mysql2/mysql2_ext.c | 14 +++-- ext/mysql2/result.c | 59 ++++++++++++------- .../connection_adapters/mysql2_adapter.rb | 42 ++++++++++--- lib/mysql2/client.rb | 15 ++++- spec/active_record/active_record_spec.rb | 6 +- spec/mysql2/client_spec.rb | 24 ++++++++ spec/mysql2/result_spec.rb | 13 ++-- 10 files changed, 165 insertions(+), 59 deletions(-) diff --git a/benchmark/query_with_mysql_casting.rb b/benchmark/query_with_mysql_casting.rb index 61546fe..f8b6dd8 100644 --- a/benchmark/query_with_mysql_casting.rb +++ b/benchmark/query_with_mysql_casting.rb @@ -45,8 +45,8 @@ Benchmark.bmbm do |x| x.report do puts "Mysql2" number_of.times do - mysql2_result = mysql2.query sql - mysql2_result.each(:symbolize_keys => true) do |res| + mysql2_result = mysql2.query sql, :symbolize_keys => true + mysql2_result.each do |res| # puts res.inspect end end diff --git a/benchmark/query_without_mysql_casting.rb b/benchmark/query_without_mysql_casting.rb index 7d0bb03..fa12bf6 100644 --- a/benchmark/query_without_mysql_casting.rb +++ b/benchmark/query_without_mysql_casting.rb @@ -17,8 +17,8 @@ Benchmark.bmbm do |x| x.report do puts "Mysql2" number_of.times do - mysql2_result = mysql2.query sql - mysql2_result.each(:symbolize_keys => true) do |res| + mysql2_result = mysql2.query sql, :symbolize_keys => true + mysql2_result.each do |res| # puts res.inspect end end diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 356c6f5..dd99784 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -3,7 +3,8 @@ VALUE cMysql2Client; extern VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; -extern ID sym_id, sym_version, sym_async; +extern ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array; +extern ID intern_merge; #define REQUIRE_OPEN_DB(_ctxt) \ if(!_ctxt->net.vio) { \ @@ -225,7 +226,12 @@ 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"))); + +#ifdef HAVE_RUBY_ENCODING_H rb_iv_set(resultObj, "@encoding", rb_iv_get(self, "@encoding")); +#endif return resultObj; } @@ -234,29 +240,31 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { fd_set fdset; int fd, retval; int async = 0; - VALUE opts; - VALUE rb_async; + VALUE opts, defaults; + MYSQL *client; - MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); + args.mysql = client; + defaults = rb_iv_get(self, "@query_options"); if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { - if ((rb_async = rb_hash_aref(opts, sym_async)) != Qnil) { - async = rb_async == Qtrue ? 1 : 0; + opts = rb_funcall(defaults, intern_merge, 1, opts); + rb_iv_set(self, "@query_options", opts); + + if (rb_hash_aref(opts, sym_async) == Qtrue) { + async = 1; } + } else { + opts = defaults; } - Check_Type(args.sql, T_STRING); #ifdef HAVE_RUBY_ENCODING_H rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); // ensure the string is in the encoding the connection is expecting args.sql = rb_str_export_to_enc(args.sql, conn_enc); #endif - Data_Get_Struct(self, MYSQL, client); - - REQUIRE_OPEN_DB(client); - - args.mysql = client; if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) { return rb_raise_mysql2_error(client); } @@ -272,15 +280,20 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { retval = rb_thread_select(fd + 1, &fdset, NULL, NULL, NULL); if (retval < 0) { - rb_sys_fail(0); + rb_sys_fail(0); } if (retval > 0) { - break; + break; } } - return rb_mysql_client_async_result(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; } diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 68e276b..284431f 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -1,16 +1,22 @@ #include VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; -ID sym_id, sym_version, sym_async; +ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array; +ID intern_merge; /* Ruby Extension initializer */ void Init_mysql2() { mMysql2 = rb_define_module("Mysql2"); cMysql2Error = rb_const_get(mMysql2, rb_intern("Error")); - sym_id = ID2SYM(rb_intern("id")); - sym_version = ID2SYM(rb_intern("version")); - sym_async = ID2SYM(rb_intern("async")); + intern_merge = rb_intern("merge"); + + 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"); diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 6a2df6c..f2afbd0 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -4,12 +4,13 @@ rb_encoding *binaryEncoding; #endif -ID sym_symbolize_keys; ID intern_new, intern_utc, 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; +extern ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { mysql2_result_wrapper * w = wrapper; @@ -85,12 +86,13 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int return rb_field; } -static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) { - VALUE rowHash, opts, block; +static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) { + VALUE rowVal; mysql2_result_wrapper * wrapper; MYSQL_ROW row; MYSQL_FIELD * fields = NULL; - unsigned int i = 0, symbolizeKeys = 0; + unsigned int i = 0; + int symbolizeKeys = 0, asArray = 0; unsigned long * fieldLengths; void * ptr; #ifdef HAVE_RUBY_ENCODING_H @@ -100,11 +102,12 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) { GetMysql2Result(self, wrapper); - if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) { - Check_Type(opts, T_HASH); - if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { - symbolizeKeys = 1; - } + if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { + symbolizeKeys = 1; + } + + if (rb_hash_aref(opts, sym_as) == sym_array) { + asArray = 1; } ptr = wrapper->result; @@ -113,7 +116,11 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) { return Qnil; } - rowHash = rb_hash_new(); + if (asArray) { + rowVal = rb_ary_new2(wrapper->numberOfFields); + } else { + rowVal = rb_hash_new(); + } fields = mysql_fetch_fields(wrapper->result); fieldLengths = mysql_fetch_lengths(wrapper->result); if (wrapper->fields == Qnil) { @@ -220,12 +227,20 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) { #endif break; } - rb_hash_aset(rowHash, field, val); + if (asArray) { + rb_ary_push(rowVal, val); + } else { + rb_hash_aset(rowVal, field, val); + } } else { - rb_hash_aset(rowHash, field, Qnil); + if (asArray) { + rb_ary_push(rowVal, Qnil); + } else { + rb_hash_aset(rowVal, field, Qnil); + } } } - return rowHash; + return rowVal; } static VALUE rb_mysql_result_fetch_fields(VALUE self) { @@ -249,18 +264,24 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { } static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { - VALUE opts, block; + VALUE defaults, opts, block; mysql2_result_wrapper * wrapper; unsigned long i; GetMysql2Result(self, wrapper); - rb_scan_args(argc, argv, "01&", &opts, &block); + defaults = rb_iv_get(self, "@query_options"); + if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) { + opts = rb_funcall(defaults, intern_merge, 1, opts); + } else { + opts = defaults; + } if (wrapper->lastRowProcessed == 0) { wrapper->numberOfRows = mysql_num_rows(wrapper->result); if (wrapper->numberOfRows == 0) { - return Qnil; + wrapper->rows = rb_ary_new(); + return wrapper->rows; } wrapper->rows = rb_ary_new2(wrapper->numberOfRows); } @@ -279,7 +300,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(argc, argv, self); + row = rb_mysql_result_fetch_row(self, opts); rb_ary_store(wrapper->rows, i, row); wrapper->lastRowProcessed++; } @@ -319,8 +340,7 @@ VALUE rb_mysql_result_to_obj(MYSQL_RES * r) { return obj; } -void init_mysql2_result() -{ +void init_mysql2_result() { cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal")); cDate = rb_const_get(rb_cObject, rb_intern("Date")); cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime")); @@ -329,7 +349,6 @@ 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); - sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); intern_new = rb_intern("new"); intern_utc = rb_intern("utc"); intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code"); diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 13e847a..01327b7 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -161,6 +161,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + configure_connection end def adapter_name @@ -263,14 +264,36 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(sql, name = nil) + result = execute(sql, name) + result.each(:as => :hash) do |r| + return r + end + end + + # Returns a single value from a record + def select_value(sql, name = nil) + result = execute(sql, name) + if first = result.first + first.first + end + end + + # Returns an array of the values of the first column in a select: + # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(sql, name = nil) - select(sql, name).map { |row| row.values.first } + execute(sql, name).map { |row| row.first } end + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. def select_rows(sql, name = nil) - select(sql, name).map { |row| row.values } + execute(sql, name).to_a end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) if name == :skip_logging @connection.query(sql) @@ -393,8 +416,8 @@ module ActiveRecord def tables(name = nil) tables = [] - execute("SHOW TABLES", name).each(:symbolize_keys => true) do |field| - tables << field.values.first + execute("SHOW TABLES", name).each do |field| + tables << field.first end tables end @@ -407,7 +430,7 @@ module ActiveRecord indexes = [] current_index = nil result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) - result.each(:symbolize_keys => true) do |row| + result.each(:symbolize_keys => true, :as => :hash) do |row| if current_index != row[:Key_name] next if row[:Key_name] == PRIMARY # skip the primary key current_index = row[:Key_name] @@ -423,7 +446,7 @@ module ActiveRecord sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] result = execute(sql, :skip_logging) - result.each(:symbolize_keys => true) { |field| + result.each(:symbolize_keys => true, :as => :hash) { |field| columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") } columns @@ -520,7 +543,7 @@ module ActiveRecord def pk_and_sequence_for(table) keys = [] result = execute("describe #{quote_table_name(table)}") - result.each(:symbolize_keys => true) do |row| + result.each(:symbolize_keys => true, :as => :hash) do |row| keys << row[:Field] if row[:Key] == "PRI" end keys.length == 1 ? [keys.first, nil] : nil @@ -574,6 +597,7 @@ module ActiveRecord end def configure_connection + @connection.query_options.merge!(:as => :array) encoding = @config[:encoding] execute("SET NAMES '#{encoding}'", :skip_logging) if encoding @@ -582,8 +606,10 @@ module ActiveRecord execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) end + # Returns an array of record hashes with the column names as keys and + # column values as values. def select(sql, name = nil) - execute(sql, name).to_a + result = execute(sql, name).each(:as => :hash) end def supports_views? diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index d80bb89..d2c380e 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -1,6 +1,15 @@ module Mysql2 class Client - def initialize opts = {} + attr_reader :query_options + @@default_query_options = { + :symbolize_keys => false, + :async => false, + :as => :hash + } + + def initialize(opts = {}) + @query_options = @@default_query_options.dup + init_connection [:reconnect, :connect_timeout].each do |key| @@ -23,6 +32,10 @@ module Mysql2 connect user, pass, host, port, database, socket end + def self.default_query_options + @@default_query_options + end + # NOTE: from ruby-mysql if defined? Encoding CHARSET_MAP = { diff --git a/spec/active_record/active_record_spec.rb b/spec/active_record/active_record_spec.rb index 922ac7e..2b6df1b 100644 --- a/spec/active_record/active_record_spec.rb +++ b/spec/active_record/active_record_spec.rb @@ -21,8 +21,8 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do end it "should be able to execute a raw query" do - @connection.execute("SELECT 1 as one").first['one'].should eql(1) - @connection.execute("SELECT NOW() as n").first['n'].class.should eql(Time) + @connection.execute("SELECT 1 as one").first.first.should eql(1) + @connection.execute("SELECT NOW() as n").first.first.class.should eql(Time) end end @@ -72,7 +72,7 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do end after(:all) do - Mysql2Test2.connection.execute("DELETE FROM mysql2_test WHERE id=#{@test_result['id']}") + Mysql2Test2.connection.execute("DELETE FROM mysql2_test WHERE id=#{@test_result.first}") end it "default value should be cast to the expected type of the field" do diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 7898390..262dc6f 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -14,6 +14,10 @@ describe Mysql2::Client do end end + it "should have a global default_query_options hash" do + Mysql2::Client.should respond_to(:default_query_options) + end + it "should be able to connect via SSL options" do pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.") ssl_client = nil @@ -49,6 +53,26 @@ describe Mysql2::Client do @client.should respond_to(:query) end + context "#query" do + it "should accept an options hash that inherits from Mysql2::Client.default_query_options" do + @client.query "SELECT 1", :something => :else + @client.query_options.should eql(@client.query_options.merge(:something => :else)) + end + + it "should return results as a hash by default" do + @client.query("SELECT 1").first.class.should eql(Hash) + end + + it "should be able to return results as an array" do + @client.query("SELECT 1", :as => :array).first.class.should eql(Array) + @client.query("SELECT 1").each(:as => :array) + end + + 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 + end + it "should respond to #escape" do @client.should respond_to(:escape) end diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index 900101a..3d553b2 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Mysql2::Result do - before(:all) do + before(:each) do @client = Mysql2::Client.new :host => "localhost", :username => "root" end @@ -37,18 +37,23 @@ describe Mysql2::Result do it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do @result.each(:symbolize_keys => true) do |row| - row.class.should eql(Hash) row.keys.first.class.should eql(Symbol) end end + it "should be able to return results as an array" do + @result.each(:as => :array) do |row| + row.class.should eql(Array) + end + end + it "should cache previously yielded results" do @result.first.should eql(@result.first) end end context "#fields" do - before(:all) do + before(:each) do @client.query "USE test" @test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") end @@ -64,7 +69,7 @@ describe Mysql2::Result do end context "row data type mapping" do - before(:all) do + before(:each) do @client.query "USE test" @client.query %[ CREATE TABLE IF NOT EXISTS mysql2_test ( From 6142336e5947e19182c8ec889b03b28c0bcbb45a Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sun, 1 Aug 2010 20:23:27 -0700 Subject: [PATCH 02/10] use a default (year 2000) for TIME field casting to a Time object, like AR does --- ext/mysql2/result.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index f2afbd0..6e5434d 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -158,7 +158,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) { 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, intern_utc, 6, INT2NUM(0), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, intern_utc, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); break; } case MYSQL_TYPE_TIMESTAMP: // TIMESTAMP field From a9e2fbcc7836edda4dc4a8c91f8bda86bc9d82da Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sun, 1 Aug 2010 22:05:11 -0700 Subject: [PATCH 03/10] update readme to reflect recent API changes --- README.rdoc | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/README.rdoc b/README.rdoc index 8705012..94a5fa9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -58,6 +58,48 @@ How about with symbolized keys? # do something with row, it's ready to rock end +== Cascading config + +The default config hash is at: + + Mysql2::Client.default_query_options + +which defaults to: + + {:async => false, :as => :hash, :symbolize_keys => false} + +that can be used as so: + + # these are the defaults all Mysql2::Client instances inherit + Mysql2::Client.default_query_options.merge!(:as => :array) + +or + + # this will change the defaults for all future results returned by the #query method _for this connection only_ + c = Mysql2::Client.new + c.query_options.merge(:symbolize_keys => true) + +or + + # this will set the options for the Mysql2::Result instance returned from the #query method + c = Mysql2::Client.new + c.query(sql, :symbolize_keys => true) + +== Result types + +=== Array of Arrays + +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} + +=== 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. +If you'd like to see either of these (or others), open an issue and start bugging me about it ;) + == Async Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries. @@ -151,11 +193,11 @@ then iterating over every row using an #each like method yielding a block: # These results are from the query_with_mysql_casting.rb script in the benchmarks folder user system total real Mysql2 - 0.890000 0.190000 1.080000 ( 2.028887) + 0.750000 0.180000 0.930000 ( 1.821655) do_mysql - 1.740000 0.220000 1.960000 ( 2.909290) + 1.650000 0.200000 1.850000 ( 2.811357) Mysql - 7.330000 0.350000 7.680000 ( 8.013160) + 7.500000 0.210000 7.710000 ( 8.065871) == Special Thanks From f3151db928e5db151a6d2a0b369becc3d441fdd4 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sun, 1 Aug 2010 23:18:38 -0700 Subject: [PATCH 04/10] update gemspec to include client.c/h --- mysql2.gemspec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mysql2.gemspec b/mysql2.gemspec index 1d67599..318814b 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Brian Lopez"] - s.date = %q{2010-07-17} + s.date = %q{2010-08-01} s.email = %q{seniorlopez@gmail.com} s.extensions = ["ext/mysql2/extconf.rb"] s.extra_rdoc_files = [ @@ -29,6 +29,8 @@ Gem::Specification.new do |s| "benchmark/sequel.rb", "benchmark/setup_db.rb", "examples/eventmachine.rb", + "ext/mysql2/client.c", + "ext/mysql2/client.h", "ext/mysql2/extconf.rb", "ext/mysql2/mysql2_ext.c", "ext/mysql2/mysql2_ext.h", From fc6c24a20cf13b1d952a2b0a90d9ec9302fe60aa Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 01:19:28 -0700 Subject: [PATCH 05/10] add support for configuring which timezone Time objects should be created in --- README.rdoc | 4 ++ benchmark/active_record.rb | 2 +- ext/mysql2/mysql2_ext.c | 6 ++- ext/mysql2/result.c | 48 ++++++++++++------- .../connection_adapters/mysql2_adapter.rb | 3 ++ lib/mysql2/client.rb | 3 +- spec/active_record/active_record_spec.rb | 10 ++-- 7 files changed, 50 insertions(+), 26 deletions(-) diff --git a/README.rdoc b/README.rdoc index 94a5fa9..e21a0f9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -100,6 +100,10 @@ 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 + +You can set the :timezone option to :local or :utc to tell Mysql2 which timezone you'd like to have Time objects in + == Async Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries. diff --git a/benchmark/active_record.rb b/benchmark/active_record.rb index f3297de..a52e118 100644 --- a/benchmark/active_record.rb +++ b/benchmark/active_record.rb @@ -5,7 +5,7 @@ require 'rubygems' require 'benchmark' require 'active_record' -ActiveRecord::Base.default_timezone = 'Pacific Time (US & Canada)' +ActiveRecord::Base.default_timezone = :local ActiveRecord::Base.time_zone_aware_attributes = true number_of = 10 diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 284431f..b027f45 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -1,7 +1,8 @@ #include VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; -ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array; +ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, + sym_array, sym_timezone, sym_utc, sym_local; ID intern_merge; /* Ruby Extension initializer */ @@ -11,6 +12,9 @@ void Init_mysql2() { 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")); diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 6e5434d..02f70f0 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -4,12 +4,12 @@ rb_encoding *binaryEncoding; #endif -ID intern_new, intern_utc, intern_encoding_from_charset_code; +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; +extern ID sym_symbolize_keys, sym_as, sym_array, sym_timezone, sym_local, sym_utc; extern ID intern_merge; static void rb_mysql_result_mark(void * wrapper) { @@ -86,13 +86,12 @@ 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, VALUE opts) { +static VALUE rb_mysql_result_fetch_row(VALUE self, ID timezone, int symbolizeKeys, int asArray) { VALUE rowVal; mysql2_result_wrapper * wrapper; MYSQL_ROW row; MYSQL_FIELD * fields = NULL; unsigned int i = 0; - int symbolizeKeys = 0, asArray = 0; unsigned long * fieldLengths; void * ptr; #ifdef HAVE_RUBY_ENCODING_H @@ -102,14 +101,6 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) { GetMysql2Result(self, wrapper); - if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { - symbolizeKeys = 1; - } - - if (rb_hash_aref(opts, sym_as) == sym_array) { - asArray = 1; - } - ptr = wrapper->result; row = (MYSQL_ROW)rb_thread_blocking_region(nogvl_fetch_row, ptr, RUBY_UBF_IO, 0); if (row == NULL) { @@ -158,7 +149,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) { 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, intern_utc, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, timezone, 6, INT2NUM(2000), INT2NUM(1), INT2NUM(1), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); break; } case MYSQL_TYPE_TIMESTAMP: // TIMESTAMP field @@ -172,7 +163,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) { rb_raise(cMysql2Error, "Invalid date: %s", row[i]); val = Qnil; } else { - val = rb_funcall(rb_cTime, intern_utc, 6, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); + val = rb_funcall(rb_cTime, timezone, 6, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec)); } } break; @@ -264,9 +255,11 @@ 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; + VALUE defaults, opts, block, timezoneVal; + ID timezone; mysql2_result_wrapper * wrapper; unsigned long i; + int symbolizeKeys = 0, asArray = 0; GetMysql2Result(self, wrapper); @@ -277,6 +270,24 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { opts = defaults; } + if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { + symbolizeKeys = 1; + } + + if (rb_hash_aref(opts, sym_as) == sym_array) { + asArray = 1; + } + + timezoneVal = rb_hash_aref(opts, sym_timezone); + if (timezoneVal == sym_local) { + timezone = intern_local; + } else if (timezoneVal == sym_utc) { + timezone = intern_utc; + } else { + rb_warn(":timezone config option must be :utc or :local - defaulting to :local"); + timezone = intern_local; + } + if (wrapper->lastRowProcessed == 0) { wrapper->numberOfRows = mysql_num_rows(wrapper->result); if (wrapper->numberOfRows == 0) { @@ -300,7 +311,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, opts); + row = rb_mysql_result_fetch_row(self, timezone, symbolizeKeys, asArray); rb_ary_store(wrapper->rows, i, row); wrapper->lastRowProcessed++; } @@ -349,8 +360,9 @@ 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_new = rb_intern("new"); - intern_utc = rb_intern("utc"); + 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"); #ifdef HAVE_RUBY_ENCODING_H diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 01327b7..8f6d159 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -295,6 +295,9 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. 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 if name == :skip_logging @connection.query(sql) else diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index d2c380e..f2db76e 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -4,7 +4,8 @@ module Mysql2 @@default_query_options = { :symbolize_keys => false, :async => false, - :as => :hash + :as => :hash, + :timezone => :local } def initialize(opts = {}) diff --git a/spec/active_record/active_record_spec.rb b/spec/active_record/active_record_spec.rb index 2b6df1b..782d581 100644 --- a/spec/active_record/active_record_spec.rb +++ b/spec/active_record/active_record_spec.rb @@ -28,7 +28,7 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do context "columns" do before(:all) do - ActiveRecord::Base.default_timezone = 'Pacific Time (US & Canada)' + ActiveRecord::Base.default_timezone = :local ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.establish_connection(:adapter => 'mysql2', :database => 'test') Mysql2Test2.connection.execute %[ @@ -89,9 +89,9 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do test.double_test.should eql('1.0000'.to_f) test.decimal_test.should eql(BigDecimal.new('1.0000')) test.date_test.should eql(Date.parse('2010-01-01')) - test.date_time_test.should eql(DateTime.parse('2010-01-01 00:00:00')) + test.date_time_test.should eql(Time.local(2010,1,1,0,0,0)) test.timestamp_test.should be_nil - test.time_test.class.should eql(DateTime) + test.time_test.class.should eql(Time) test.year_test.should eql(2010) test.char_test.should eql('abcdefghij') test.varchar_test.should eql('abcdefghij') @@ -125,8 +125,8 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do test.double_test.should eql('1.0000'.to_f) test.decimal_test.should eql(BigDecimal.new('1.0000')) test.date_test.should eql(Date.parse('2010-01-01')) - test.date_time_test.should eql(Time.utc(2010,1,1,0,0,0)) - test.timestamp_test.class.should eql(ActiveSupport::TimeWithZone) + test.date_time_test.should eql(Time.local(2010,1,1,0,0,0)) + test.timestamp_test.class.should eql(Time) test.time_test.class.should eql(Time) test.year_test.should eql(2010) test.char_test.should eql('abcdefghij') From 1086233cc30859e9c9f268c6012b98677192d481 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 01:21:09 -0700 Subject: [PATCH 06/10] bugfix for how arel resolves compiler paths --- 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 8f6d159..9ee1000 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -131,7 +131,7 @@ module ActiveRecord cattr_accessor :emulate_booleans self.emulate_booleans = true - ADAPTER_NAME = 'MySQL2' + ADAPTER_NAME = 'Mysql2' PRIMARY = "PRIMARY" LOST_CONNECTION_ERROR_MESSAGES = [ From 00ab233606f2d586873ce8ea9afb7d1119dd6a8a Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 01:26:04 -0700 Subject: [PATCH 07/10] remove performance overrides of select_one, select_value, and select_values in the AR adapter for now --- .../connection_adapters/mysql2_adapter.rb | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 9ee1000..485cb71 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -264,28 +264,33 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - # Returns a record hash with the column names as keys and column values - # as values. - def select_one(sql, name = nil) - result = execute(sql, name) - result.each(:as => :hash) do |r| - return r - end - end - - # Returns a single value from a record - def select_value(sql, name = nil) - result = execute(sql, name) - if first = result.first - first.first - end - end - - # Returns an array of the values of the first column in a select: - # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(sql, name = nil) - execute(sql, name).map { |row| row.first } - end + # FIXME: re-enable the following once a "better" query_cache solution is in core + # + # The overrides below perform much better than the originals in AbstractAdapter + # because we're able to take advantage of mysql2's lazy-loading capabilities + # + # # Returns a record hash with the column names as keys and column values + # # as values. + # def select_one(sql, name = nil) + # result = execute(sql, name) + # result.each(:as => :hash) do |r| + # return r + # end + # end + # + # # Returns a single value from a record + # def select_value(sql, name = nil) + # result = execute(sql, name) + # if first = result.first + # first.first + # end + # end + # + # # Returns an array of the values of the first column in a select: + # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + # def select_values(sql, name = nil) + # execute(sql, name).map { |row| row.first } + # end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. From 074885cca1e1e41de756bae29d169dab5ebdde6d Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 01:26:15 -0700 Subject: [PATCH 08/10] cleanup in AR adapter --- lib/active_record/connection_adapters/mysql2_adapter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 485cb71..d4345a4 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -317,7 +317,7 @@ module ActiveRecord end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super sql, name + super id_value || @connection.last_id end alias :create :insert_sql @@ -617,7 +617,7 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil) - result = execute(sql, name).each(:as => :hash) + execute(sql, name).each(:as => :hash) end def supports_views? From 9756a3f41089b2f650fa8a3958f351f79035243b Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 01:43:25 -0700 Subject: [PATCH 09/10] fix typo --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index e21a0f9..990b5cd 100644 --- a/README.rdoc +++ b/README.rdoc @@ -77,7 +77,7 @@ or # this will change the defaults for all future results returned by the #query method _for this connection only_ c = Mysql2::Client.new - c.query_options.merge(:symbolize_keys => true) + c.query_options.merge!(:symbolize_keys => true) or From 5f819961226d56f3e175e540f3699faad78bb852 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 2 Aug 2010 02:07:11 -0700 Subject: [PATCH 10/10] fixes for 1.9 --- spec/active_record/active_record_spec.rb | 3 ++- spec/mysql2/result_spec.rb | 6 +----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/active_record/active_record_spec.rb b/spec/active_record/active_record_spec.rb index 782d581..055db0e 100644 --- a/spec/active_record/active_record_spec.rb +++ b/spec/active_record/active_record_spec.rb @@ -126,7 +126,8 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do test.decimal_test.should eql(BigDecimal.new('1.0000')) test.date_test.should eql(Date.parse('2010-01-01')) test.date_time_test.should eql(Time.local(2010,1,1,0,0,0)) - test.timestamp_test.class.should eql(Time) + test.timestamp_test.class.should eql(ActiveSupport::TimeWithZone) if RUBY_VERSION >= "1.9" + test.timestamp_test.class.should eql(Time) if RUBY_VERSION < "1.9" test.time_test.class.should eql(Time) test.year_test.should eql(2010) test.char_test.should eql('abcdefghij') diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index 3d553b2..8585656 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -197,11 +197,7 @@ describe Mysql2::Result do it "should return Time for a TIME value" do @test_result['time_test'].class.should eql(Time) - if RUBY_VERSION >= "1.9.2" - @test_result['time_test'].strftime("%F %T").should eql('0000-01-01 11:44:00') - else - @test_result['time_test'].strftime("%F %T").should eql('2000-01-01 11:44:00') - end + @test_result['time_test'].strftime("%F %T").should eql('2000-01-01 11:44:00') end it "should return Date for a DATE value" do