From 807875e3212edefebf88605ed119472325cf86e8 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 16 Jul 2010 10:26:43 -0700 Subject: [PATCH 01/22] make sure queries run through the Sequel logger --- lib/sequel/adapters/mysql2.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/sequel/adapters/mysql2.rb b/lib/sequel/adapters/mysql2.rb index 09d49f1..26b74bf 100644 --- a/lib/sequel/adapters/mysql2.rb +++ b/lib/sequel/adapters/mysql2.rb @@ -120,8 +120,7 @@ module Sequel # yield the connection if a block is given. def _execute(conn, sql, opts) begin - # r = log_yield(sql){conn.query(sql)} - r = conn.query(sql) + r = log_yield(sql){conn.query(sql)} if opts[:type] == :select yield r if r elsif block_given? From 224e8eff66071fb0cd45dc1bdb2de89d294e57e0 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 17 Jul 2010 18:07:59 -0700 Subject: [PATCH 02/22] Version bump to 0.1.9 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 699c6c6..1a03094 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.8 +0.1.9 From 1c02820f4b7b47bce2060d628ab99748f53c932b Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 17 Jul 2010 18:09:48 -0700 Subject: [PATCH 03/22] updating files for release --- CHANGELOG.md | 13 +++++++++++++ lib/mysql2.rb | 2 +- mysql2.gemspec | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f12c9..428147f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## 0.1.9 (HEAD) * Support async ActiveRecord access with fibers and EventMachine (mperham) +* string encoding support for 1.9, respecting Encoding.default_internal +* added support for rake-compiler (tenderlove) +* bugfixes for ActiveRecord driver +** one minor bugfix for TimeZone support +** fix the select_rows method to return what it should according to the docs (r-stu31) +* Mysql2::Client#fields method added - returns the array of field names from a resultset, as strings +* Sequel adapter +** bugfix regarding sybolized field names (Eric Wong) +** fix query logging in Sequel adapter +* Lots of nice code cleanup (tenderlove) +** Mysql2::Error definition moved to pure-Ruby +** Mysql2::client#initialize definition moved to pure-Ruby +** Mysql2::Result partially moved to pure-Ruby ## 0.1.8 (June 2nd, 2010) * fixes for AR adapter for timezone juggling diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 9fee43f..fb24cfc 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -11,5 +11,5 @@ require 'mysql2/result' # # A modern, simple and very fast Mysql library for Ruby - binding to libmysql module Mysql2 - VERSION = "0.1.8" + VERSION = "0.1.9" end diff --git a/mysql2.gemspec b/mysql2.gemspec index 6b6d12c..1d67599 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{mysql2} - s.version = "0.1.8" + s.version = "0.1.9" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Brian Lopez"] - s.date = %q{2010-07-06} + s.date = %q{2010-07-17} s.email = %q{seniorlopez@gmail.com} s.extensions = ["ext/mysql2/extconf.rb"] s.extra_rdoc_files = [ @@ -34,7 +34,9 @@ Gem::Specification.new do |s| "ext/mysql2/mysql2_ext.h", "ext/mysql2/result.c", "ext/mysql2/result.h", + "lib/active_record/connection_adapters/em_mysql2_adapter.rb", "lib/active_record/connection_adapters/mysql2_adapter.rb", + "lib/active_record/fiber_patches.rb", "lib/arel/engines/sql/compilers/mysql2_compiler.rb", "lib/mysql2.rb", "lib/mysql2/client.rb", From 3c262e7328d1e1f108d0d0cc5f87f311d7e1aff7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 17 Jul 2010 18:44:21 -0700 Subject: [PATCH 04/22] fix 0.1.9 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428147f..f90d6f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.1.9 (HEAD) +## 0.1.9 (July 17th, 2010) * Support async ActiveRecord access with fibers and EventMachine (mperham) * string encoding support for 1.9, respecting Encoding.default_internal * added support for rake-compiler (tenderlove) From 9ab1c90d2b669e7527215793c3a2bba56c7f48a8 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 20 Jul 2010 09:12:47 -0700 Subject: [PATCH 05/22] don't raise exception on close, for a closed connection --- ext/mysql2/mysql2_ext.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 79d8d17..181d6fc 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -141,7 +141,6 @@ static VALUE rb_mysql_client_close(VALUE self) { Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); rb_thread_blocking_region(nogvl_close, client, RUBY_UBF_IO, 0); return Qnil; From 3ec92096cafb4c9c188ae62dccb7c9f59c78f915 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 20 Jul 2010 09:14:37 -0700 Subject: [PATCH 06/22] MySQL -> Mysql2 naming in AR adapter --- .../connection_adapters/mysql2_adapter.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 4e2e357..334905a 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord module ConnectionAdapters class Mysql2Column < Column - BOOL = "tinyint(1)".freeze + BOOL = "tinyint(1)" def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -130,8 +130,8 @@ module ActiveRecord cattr_accessor :emulate_booleans self.emulate_booleans = true - ADAPTER_NAME = 'MySQL'.freeze - PRIMARY = "PRIMARY".freeze + ADAPTER_NAME = 'MySQL2' + PRIMARY = "PRIMARY" LOST_CONNECTION_ERROR_MESSAGES = [ "Server shutdown in progress", @@ -139,10 +139,10 @@ module ActiveRecord "Lost connection to MySQL server during query", "MySQL server has gone away" ] - QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze + QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze, + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "int", :limit => 4 }, @@ -263,7 +263,7 @@ module ActiveRecord select(sql, name).map { |row| row.values } end - # Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it. + # Executes a SQL query and returns a Mysql2::Result object. Note that you have to free the Result object after you're done using it. def execute(sql, name = nil) if name == :skip_logging @connection.query(sql) From 678ff1cee915a025355b03a4f2841b8d8e8e09e4 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 20 Jul 2010 18:47:44 -0700 Subject: [PATCH 07/22] fall back to blocking behavior for em_mysql2 AR adapter if EM isn't running --- .../connection_adapters/em_mysql2_adapter.rb | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/active_record/connection_adapters/em_mysql2_adapter.rb b/lib/active_record/connection_adapters/em_mysql2_adapter.rb index 0077e13..e26c9df 100644 --- a/lib/active_record/connection_adapters/em_mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/em_mysql2_adapter.rb @@ -42,17 +42,21 @@ module Mysql2 end def query(sql, opts={}) - super(sql, opts.merge(:async => true)) - deferable = ::EM::DefaultDeferrable.new - ::EM.watch(self.socket, Watcher, self, deferable).notify_readable = true - fiber = Fiber.current - deferable.callback do |result| - fiber.resume(result) + if EM.reactor_running? + super(sql, opts.merge(:async => true)) + deferable = ::EM::DefaultDeferrable.new + ::EM.watch(self.socket, Watcher, self, deferable).notify_readable = true + fiber = Fiber.current + deferable.callback do |result| + fiber.resume(result) + end + deferable.errback do |err| + fiber.resume(err) + end + Fiber.yield + else + super(sql, opts) end - deferable.errback do |err| - fiber.resume(err) - end - Fiber.yield end end end From 70a7298dc34ac3728df076b8462d2152cb8834fe Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 21 Jul 2010 11:46:44 -0700 Subject: [PATCH 08/22] force reconnect behavior to true, remove open connection checks since libmysql will handle this for us now --- ext/mysql2/mysql2_ext.c | 14 -------------- lib/mysql2/client.rb | 9 +++++---- spec/mysql2/client_spec.rb | 7 ------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 181d6fc..f15eee0 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -4,12 +4,6 @@ VALUE mMysql2, cMysql2Client; VALUE cMysql2Error, intern_encoding_from_charset; ID sym_id, sym_version, sym_async; -#define REQUIRE_OPEN_DB(_ctxt) \ - if(!_ctxt->net.vio) { \ - rb_raise(cMysql2Error, "closed MySQL connection"); \ - return Qnil; \ - } - /* * non-blocking mysql_*() functions that we won't be wrapping since * they do not appear to hit the network nor issue any interruptible @@ -186,7 +180,6 @@ static VALUE rb_mysql_client_async_result(VALUE self) { Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { return rb_raise_mysql2_error(client); } @@ -229,8 +222,6 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { 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); @@ -279,7 +270,6 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), RSTRING_LEN(str)); if (newLen == oldLen) { // no need to return a new ruby string if nothing changed @@ -324,7 +314,6 @@ static VALUE rb_mysql_client_server_info(VALUE self) { #endif Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); version = rb_hash_new(); rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client))); @@ -342,21 +331,18 @@ static VALUE rb_mysql_client_server_info(VALUE self) { static VALUE rb_mysql_client_socket(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); return INT2NUM(client->net.fd); } static VALUE rb_mysql_client_last_id(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); return ULL2NUM(mysql_insert_id(client)); } static VALUE rb_mysql_client_affected_rows(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); return ULL2NUM(mysql_affected_rows(client)); } diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index e49ed4b..b98aa21 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -3,13 +3,14 @@ module Mysql2 def initialize opts = {} init_connection - [:reconnect, :connect_timeout].each do |key| - next unless opts.key?(key) - send(:"#{key}=", opts[key]) - end + self.connect_timeout = opts[:connect_timeout] + # force the encoding to utf8 self.charset_name = opts[:encoding] || 'utf8' + # force reconnection behavior in libmysql + self.reconnect = true + ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslciper)) user = opts[:username] diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 7165f48..065c129 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -45,13 +45,6 @@ describe Mysql2::Client do @client.close.should be_nil end - it "should raise an exception when closed twice" do - @client.close.should be_nil - lambda { - @client.close - }.should raise_error(Mysql2::Error) - end - it "should respond to #query" do @client.should respond_to :query end From cd1a2bf9ff6b47abfa1c9838177577ab6c980ab9 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 21 Jul 2010 12:13:27 -0700 Subject: [PATCH 09/22] Revert "force reconnect behavior to true, remove open connection checks since libmysql will handle this for us now" This reverts commit 70a7298dc34ac3728df076b8462d2152cb8834fe. --- ext/mysql2/mysql2_ext.c | 14 ++++++++++++++ lib/mysql2/client.rb | 9 ++++----- spec/mysql2/client_spec.rb | 7 +++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index f15eee0..181d6fc 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -4,6 +4,12 @@ VALUE mMysql2, cMysql2Client; VALUE cMysql2Error, intern_encoding_from_charset; ID sym_id, sym_version, sym_async; +#define REQUIRE_OPEN_DB(_ctxt) \ + if(!_ctxt->net.vio) { \ + rb_raise(cMysql2Error, "closed MySQL connection"); \ + return Qnil; \ + } + /* * non-blocking mysql_*() functions that we won't be wrapping since * they do not appear to hit the network nor issue any interruptible @@ -180,6 +186,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) { Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { return rb_raise_mysql2_error(client); } @@ -222,6 +229,8 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { 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); @@ -270,6 +279,7 @@ static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), RSTRING_LEN(str)); if (newLen == oldLen) { // no need to return a new ruby string if nothing changed @@ -314,6 +324,7 @@ static VALUE rb_mysql_client_server_info(VALUE self) { #endif Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); version = rb_hash_new(); rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client))); @@ -331,18 +342,21 @@ static VALUE rb_mysql_client_server_info(VALUE self) { static VALUE rb_mysql_client_socket(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); return INT2NUM(client->net.fd); } static VALUE rb_mysql_client_last_id(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); return ULL2NUM(mysql_insert_id(client)); } static VALUE rb_mysql_client_affected_rows(VALUE self) { MYSQL * client; Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); return ULL2NUM(mysql_affected_rows(client)); } diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index b98aa21..e49ed4b 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -3,14 +3,13 @@ module Mysql2 def initialize opts = {} init_connection - self.connect_timeout = opts[:connect_timeout] - + [:reconnect, :connect_timeout].each do |key| + next unless opts.key?(key) + send(:"#{key}=", opts[key]) + end # force the encoding to utf8 self.charset_name = opts[:encoding] || 'utf8' - # force reconnection behavior in libmysql - self.reconnect = true - ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslciper)) user = opts[:username] diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 065c129..7165f48 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -45,6 +45,13 @@ describe Mysql2::Client do @client.close.should be_nil end + it "should raise an exception when closed twice" do + @client.close.should be_nil + lambda { + @client.close + }.should raise_error(Mysql2::Error) + end + it "should respond to #query" do @client.should respond_to :query end From 3da993ce5321909eb42c8e1ebd7a6aa6a1344d69 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 21 Jul 2010 13:13:08 -0700 Subject: [PATCH 10/22] prevent ruby warning when attempting to lookup the @encoding ivar before it exists --- lib/mysql2/client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index e49ed4b..95dd6e4 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -9,6 +9,7 @@ module Mysql2 end # force the encoding to utf8 self.charset_name = opts[:encoding] || 'utf8' + @encoding = nil ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslciper)) From c614c89216d7afee1c25b51741a835b163776396 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 21 Jul 2010 13:14:43 -0700 Subject: [PATCH 11/22] remove spec checking for exception raised when closing a closed connection as we don't do that anymore --- spec/mysql2/client_spec.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 7165f48..065c129 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -45,13 +45,6 @@ describe Mysql2::Client do @client.close.should be_nil end - it "should raise an exception when closed twice" do - @client.close.should be_nil - lambda { - @client.close - }.should raise_error(Mysql2::Error) - end - it "should respond to #query" do @client.should respond_to :query end From a878ba027227d7f80add9a429ea7609d6b647c70 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 21 Jul 2010 13:17:32 -0700 Subject: [PATCH 12/22] make sure we create @encoding before setting encoding --- lib/mysql2/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index 95dd6e4..d80bb89 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -8,8 +8,8 @@ module Mysql2 send(:"#{key}=", opts[key]) end # force the encoding to utf8 - self.charset_name = opts[:encoding] || 'utf8' @encoding = nil + self.charset_name = opts[:encoding] || 'utf8' ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslciper)) From 20ef2e5e27acc69a827ca66dce0d906863e052d1 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Fri, 23 Jul 2010 23:43:02 -0700 Subject: [PATCH 13/22] cleanup connection handling in AR adapter, this should finally resolve GH#31 --- .../connection_adapters/mysql2_adapter.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 334905a..d227ce8 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -238,7 +238,8 @@ module ActiveRecord end def reconnect! - reset! + disconnect! + connect end def disconnect! @@ -250,7 +251,12 @@ module ActiveRecord def reset! disconnect! - @connection = Mysql2::Client.new(@config) + connect + end + + # this is set to true in 2.3, but we don't want it to be + def requires_reloading? + false end # DATABASE STATEMENTS ====================================== @@ -263,7 +269,6 @@ module ActiveRecord select(sql, name).map { |row| row.values } end - # Executes a SQL query and returns a Mysql2::Result object. Note that you have to free the Result object after you're done using it. def execute(sql, name = nil) if name == :skip_logging @connection.query(sql) @@ -547,7 +552,6 @@ module ActiveRecord end end - # TODO: implement error_number method on Mysql2::Exception def translate_exception(exception, message) return super unless exception.respond_to?(:error_number) @@ -563,7 +567,8 @@ module ActiveRecord private def connect - # no-op + @connection = Mysql2::Client.new(@config) + configure_connection end def configure_connection From 56ecff58bf3850c504d199faf79721032bc85cfa Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 26 Jul 2010 00:10:49 -0700 Subject: [PATCH 14/22] don't attempt a query unless a connection exists to do it on --- lib/active_record/connection_adapters/mysql2_adapter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index d227ce8..dc5b890 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -231,6 +231,7 @@ module ActiveRecord # CONNECTION MANAGEMENT ==================================== def active? + return false unless @connection @connection.query 'select 1' true rescue Mysql2::Error From a45cb10deca8125e56504670f080b89a46731a91 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 27 Jul 2010 09:22:09 -0700 Subject: [PATCH 15/22] move connection into method --- lib/active_record/connection_adapters/mysql2_adapter.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index dc5b890..8d720e5 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -255,11 +255,6 @@ module ActiveRecord connect end - # this is set to true in 2.3, but we don't want it to be - def requires_reloading? - false - end - # DATABASE STATEMENTS ====================================== def select_values(sql, name = nil) @@ -569,7 +564,6 @@ module ActiveRecord private def connect @connection = Mysql2::Client.new(@config) - configure_connection end def configure_connection @@ -601,4 +595,4 @@ module ActiveRecord end end end -end \ No newline at end of file +end From 9241f35df3dab04fa83d54c923539d8df67fab26 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 27 Jul 2010 09:26:07 -0700 Subject: [PATCH 16/22] fixes after merge --- lib/active_record/connection_adapters/mysql2_adapter.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 8d720e5..3de2d0e 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -243,6 +243,11 @@ module ActiveRecord connect end + # this is set to true in 2.3, but we don't want it to be + def requires_reloading? + false + end + def disconnect! unless @connection.nil? @connection.close @@ -564,6 +569,7 @@ module ActiveRecord private def connect @connection = Mysql2::Client.new(@config) + configure_connection end def configure_connection From dd138de8e1bfb3730ffb7c7266760b9f7e62cde3 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 28 Jul 2010 11:31:32 -0700 Subject: [PATCH 17/22] invalidate the file descriptor on close --- ext/mysql2/mysql2_ext.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 181d6fc..3441b04 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -88,7 +88,9 @@ static void rb_mysql_client_free(void * ptr) { } static VALUE nogvl_close(void * ptr) { - mysql_close((MYSQL *)ptr); + MYSQL *client = (MYSQL *)ptr; + mysql_close(client); + client->net.fd = -1; return Qnil; } From 89401acff42e4baf7e3b2921995a2be316631837 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 28 Jul 2010 14:54:33 -0700 Subject: [PATCH 18/22] make sure we tell AR the proper attribute types for SET and BIT fields as well --- lib/active_record/connection_adapters/mysql2_adapter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 3de2d0e..13e847a 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -86,8 +86,9 @@ module ActiveRecord private def simplified_type(field_type) return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) - return :string if field_type =~ /enum/i + return :string if field_type =~ /enum/i or field_type =~ /set/i return :integer if field_type =~ /year/i + return :binary if field_type =~ /bit/i super end From 1e258c9d9c5f84a596bb78d1f9184bab21c314f8 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 29 Jul 2010 08:57:05 -0700 Subject: [PATCH 19/22] don't forget to turn on time_zone_aware_attributes in AR benchmark --- benchmark/active_record.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/benchmark/active_record.rb b/benchmark/active_record.rb index d96119d..f3297de 100644 --- a/benchmark/active_record.rb +++ b/benchmark/active_record.rb @@ -5,6 +5,9 @@ require 'rubygems' require 'benchmark' require 'active_record' +ActiveRecord::Base.default_timezone = 'Pacific Time (US & Canada)' +ActiveRecord::Base.time_zone_aware_attributes = true + number_of = 10 mysql2_opts = { :adapter => 'mysql2', From 9eb39173288b661816fdeb16ce7b806dfe2de2cb Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 29 Jul 2010 23:22:42 -0700 Subject: [PATCH 20/22] get rid of some warnings --- spec/mysql2/client_spec.rb | 12 ++++++------ spec/mysql2/result_spec.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 065c129..7898390 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -38,7 +38,7 @@ describe Mysql2::Client do end it "should respond to #close" do - @client.should respond_to :close + @client.should respond_to(:close) end it "should be able to close properly" do @@ -46,11 +46,11 @@ describe Mysql2::Client do end it "should respond to #query" do - @client.should respond_to :query + @client.should respond_to(:query) end it "should respond to #escape" do - @client.should respond_to :escape + @client.should respond_to(:escape) end it "#escape should return a new SQL-escape version of the passed string" do @@ -63,7 +63,7 @@ describe Mysql2::Client do end it "should respond to #info" do - @client.should respond_to :info + @client.should respond_to(:info) end it "#info should return a hash containing the client version ID and String" do @@ -95,7 +95,7 @@ describe Mysql2::Client do end it "should respond to #server_info" do - @client.should respond_to :server_info + @client.should respond_to(:server_info) end it "#server_info should return a hash containing the client version ID and String" do @@ -127,7 +127,7 @@ describe Mysql2::Client do end it "should respond to #socket" do - @client.should respond_to :socket + @client.should respond_to(:socket) end it "#socket should return a Fixnum (file descriptor from C)" do diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index 2fa0bb8..900101a 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -15,7 +15,7 @@ describe Mysql2::Result do end it "should respond to #each" do - @result.should respond_to :each + @result.should respond_to(:each) end it "should raise a Mysql2::Error exception upon a bad query" do From 8a642a35055cc4874d8eba0c5c1a61b37cdb950a Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 29 Jul 2010 23:24:09 -0700 Subject: [PATCH 21/22] ignore pre-compiled scripts from rbx --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 248d1ab..e4d4e35 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ Makefile *.bundle *.so *.a +*.rbc mkmf.log pkg/ From c6230c3cec4fd0bacad28578a8e5e8a2d33646bd Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 29 Jul 2010 23:25:15 -0700 Subject: [PATCH 22/22] pluck out Mysql2::Client definition into it's own c/h file --- ext/mysql2/client.c | 503 ++++++++++++++++++++++++++++++++++++++++ ext/mysql2/client.h | 34 +++ ext/mysql2/mysql2_ext.c | 496 +-------------------------------------- ext/mysql2/mysql2_ext.h | 68 +----- ext/mysql2/result.c | 14 +- ext/mysql2/result.h | 12 + 6 files changed, 566 insertions(+), 561 deletions(-) create mode 100644 ext/mysql2/client.c create mode 100644 ext/mysql2/client.h diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c new file mode 100644 index 0000000..356c6f5 --- /dev/null +++ b/ext/mysql2/client.c @@ -0,0 +1,503 @@ +#include +#include + +VALUE cMysql2Client; +extern VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; +extern ID sym_id, sym_version, sym_async; + +#define REQUIRE_OPEN_DB(_ctxt) \ + if(!_ctxt->net.vio) { \ + rb_raise(cMysql2Error, "closed MySQL connection"); \ + return Qnil; \ + } + +/* + * used to pass all arguments to mysql_real_connect while inside + * rb_thread_blocking_region + */ +struct nogvl_connect_args { + MYSQL *mysql; + const char *host; + const char *user; + const char *passwd; + const char *db; + unsigned int port; + const char *unix_socket; + unsigned long client_flag; +}; + +/* + * used to pass all arguments to mysql_send_query while inside + * rb_thread_blocking_region + */ +struct nogvl_send_query_args { + MYSQL *mysql; + VALUE sql; +}; + +/* + * non-blocking mysql_*() functions that we won't be wrapping since + * they do not appear to hit the network nor issue any interruptible + * or blocking system calls. + * + * - mysql_affected_rows() + * - mysql_error() + * - mysql_fetch_fields() + * - mysql_fetch_lengths() - calls cli_fetch_lengths or emb_fetch_lengths + * - mysql_field_count() + * - mysql_get_client_info() + * - mysql_get_client_version() + * - mysql_get_server_info() + * - mysql_get_server_version() + * - mysql_insert_id() + * - mysql_num_fields() + * - mysql_num_rows() + * - mysql_options() + * - mysql_real_escape_string() + * - mysql_ssl_set() + */ + +static VALUE rb_raise_mysql2_error(MYSQL *client) { + VALUE e = rb_exc_new2(cMysql2Error, mysql_error(client)); + rb_funcall(e, rb_intern("error_number="), 1, INT2NUM(mysql_errno(client))); + rb_funcall(e, rb_intern("sql_state="), 1, rb_tainted_str_new2(mysql_sqlstate(client))); + rb_exc_raise(e); + return Qnil; +} + +static VALUE nogvl_init(void *ptr) { + MYSQL * client = (MYSQL *)ptr; + + /* may initialize embedded server and read /etc/services off disk */ + mysql_init(client); + + return client ? Qtrue : Qfalse; +} + +static VALUE nogvl_connect(void *ptr) { + struct nogvl_connect_args *args = ptr; + MYSQL *client; + + client = mysql_real_connect(args->mysql, args->host, + args->user, args->passwd, + args->db, args->port, args->unix_socket, + args->client_flag); + + return client ? Qtrue : Qfalse; +} + +static void rb_mysql_client_free(void * ptr) { + MYSQL * client = (MYSQL *)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; + + 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); + if (flags > 0 && !(flags & O_NONBLOCK)) + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } + + /* It's safe to call mysql_close() on an already closed connection. */ + mysql_close(client); + xfree(ptr); +} + +static VALUE nogvl_close(void * ptr) { + MYSQL *client = (MYSQL *)ptr; + mysql_close(client); + client->net.fd = -1; + return Qnil; +} + +static VALUE allocate(VALUE klass) { + MYSQL * client; + + return Data_Make_Struct( + klass, + MYSQL, + NULL, + rb_mysql_client_free, + client + ); +} + +static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket) { + MYSQL * client; + struct nogvl_connect_args args; + + Data_Get_Struct(self, MYSQL, client); + + args.host = NIL_P(host) ? "localhost" : StringValuePtr(host); + args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket); + args.port = NIL_P(port) ? 3306 : NUM2INT(port); + 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.client_flag = 0; + + if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) + { + // unable to connect + return rb_raise_mysql2_error(client); + } + + return self; +} + +/* + * Immediately disconnect from the server, normally the garbage collector + * will disconnect automatically when a connection is no longer needed. + * Explicitly closing this will free up server resources sooner than waiting + * for the garbage collector. + */ +static VALUE rb_mysql_client_close(VALUE self) { + MYSQL *client; + + Data_Get_Struct(self, MYSQL, client); + + rb_thread_blocking_region(nogvl_close, client, RUBY_UBF_IO, 0); + + return Qnil; +} + +/* + * mysql_send_query is unlikely to block since most queries are small + * enough to fit in a socket buffer, but sometimes large UPDATE and + * INSERTs will cause the process to block + */ +static VALUE nogvl_send_query(void *ptr) { + struct nogvl_send_query_args *args = ptr; + int rv; + const char *sql = StringValuePtr(args->sql); + long sql_len = RSTRING_LEN(args->sql); + + rv = mysql_send_query(args->mysql, sql, sql_len); + + return rv == 0 ? Qtrue : Qfalse; +} + +/* + * even though we did rb_thread_select before calling this, a large + * response can overflow the socket buffers and cause us to eventually + * block while calling mysql_read_query_result + */ +static VALUE nogvl_read_query_result(void *ptr) { + MYSQL * client = ptr; + my_bool res = mysql_read_query_result(client); + + return res == 0 ? Qtrue : Qfalse; +} + +/* mysql_store_result may (unlikely) read rows off the socket */ +static VALUE nogvl_store_result(void *ptr) { + MYSQL * client = ptr; + return (VALUE)mysql_store_result(client); +} + +static VALUE rb_mysql_client_async_result(VALUE self) { + MYSQL * client; + MYSQL_RES * result; + + Data_Get_Struct(self, MYSQL, client); + + REQUIRE_OPEN_DB(client); + if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { + return rb_raise_mysql2_error(client); + } + + result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, client, RUBY_UBF_IO, 0); + if (result == NULL) { + if (mysql_field_count(client) != 0) { + rb_raise_mysql2_error(client); + } + return Qnil; + } + + VALUE resultObj = rb_mysql_result_to_obj(result); + rb_iv_set(resultObj, "@encoding", rb_iv_get(self, "@encoding")); + return resultObj; +} + +static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { + struct nogvl_send_query_args args; + fd_set fdset; + int fd, retval; + int async = 0; + VALUE opts; + VALUE rb_async; + + MYSQL * client; + + 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; + } + } + + 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); + } + + if (!async) { + // the below code is largely from do_mysql + // http://github.com/datamapper/do + fd = client->net.fd; + for(;;) { + FD_ZERO(&fdset); + FD_SET(fd, &fdset); + + retval = rb_thread_select(fd + 1, &fdset, NULL, NULL, NULL); + + if (retval < 0) { + rb_sys_fail(0); + } + + if (retval > 0) { + break; + } + } + + return rb_mysql_client_async_result(self); + } else { + return Qnil; + } +} + +static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { + MYSQL * client; + VALUE newStr; + unsigned long newLen, oldLen; + + Check_Type(str, T_STRING); +#ifdef HAVE_RUBY_ENCODING_H + rb_encoding *default_internal_enc = rb_default_internal_encoding(); + rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); + // ensure the string is in the encoding the connection is expecting + str = rb_str_export_to_enc(str, conn_enc); +#endif + + oldLen = RSTRING_LEN(str); + char escaped[(oldLen*2)+1]; + + Data_Get_Struct(self, MYSQL, client); + + REQUIRE_OPEN_DB(client); + newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), RSTRING_LEN(str)); + if (newLen == oldLen) { + // no need to return a new ruby string if nothing changed + return str; + } else { + newStr = rb_str_new(escaped, newLen); +#ifdef HAVE_RUBY_ENCODING_H + rb_enc_associate(newStr, conn_enc); + if (default_internal_enc) { + newStr = rb_str_export_to_enc(newStr, default_internal_enc); + } +#endif + return newStr; + } +} + +static VALUE rb_mysql_client_info(RB_MYSQL_UNUSED VALUE self) { + VALUE version = rb_hash_new(), client_info; +#ifdef HAVE_RUBY_ENCODING_H + rb_encoding *default_internal_enc = rb_default_internal_encoding(); + rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); +#endif + + rb_hash_aset(version, sym_id, LONG2NUM(mysql_get_client_version())); + client_info = rb_str_new2(mysql_get_client_info()); +#ifdef HAVE_RUBY_ENCODING_H + rb_enc_associate(client_info, conn_enc); + if (default_internal_enc) { + client_info = rb_str_export_to_enc(client_info, default_internal_enc); + } +#endif + rb_hash_aset(version, sym_version, client_info); + return version; +} + +static VALUE rb_mysql_client_server_info(VALUE self) { + MYSQL * client; + VALUE version, server_info; +#ifdef HAVE_RUBY_ENCODING_H + rb_encoding *default_internal_enc = rb_default_internal_encoding(); + rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); +#endif + + Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); + + version = rb_hash_new(); + rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client))); + server_info = rb_str_new2(mysql_get_server_info(client)); +#ifdef HAVE_RUBY_ENCODING_H + rb_enc_associate(server_info, conn_enc); + if (default_internal_enc) { + server_info = rb_str_export_to_enc(server_info, default_internal_enc); + } +#endif + rb_hash_aset(version, sym_version, server_info); + return version; +} + +static VALUE rb_mysql_client_socket(VALUE self) { + MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); + return INT2NUM(client->net.fd); +} + +static VALUE rb_mysql_client_last_id(VALUE self) { + MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); + return ULL2NUM(mysql_insert_id(client)); +} + +static VALUE rb_mysql_client_affected_rows(VALUE self) { + MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + REQUIRE_OPEN_DB(client); + return ULL2NUM(mysql_affected_rows(client)); +} + +static VALUE set_reconnect(VALUE self, VALUE value) { + my_bool reconnect; + MYSQL * client; + + Data_Get_Struct(self, MYSQL, client); + + if(!NIL_P(value)) { + reconnect = value == Qfalse ? 0 : 1; + + /* set default reconnect behavior */ + if (mysql_options(client, MYSQL_OPT_RECONNECT, &reconnect)) { + /* TODO: warning - unable to set reconnect behavior */ + rb_warn("%s\n", mysql_error(client)); + } + } + return value; +} + +static VALUE set_connect_timeout(VALUE self, VALUE value) { + unsigned int connect_timeout = 0; + MYSQL * client; + + Data_Get_Struct(self, MYSQL, client); + + 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)) { + /* TODO: warning - unable to set connection timeout */ + rb_warn("%s\n", mysql_error(client)); + } + } + return value; +} + +static VALUE set_charset_name(VALUE self, VALUE value) { + char * charset_name; + MYSQL * client; + + Data_Get_Struct(self, MYSQL, client); + +#ifdef HAVE_RUBY_ENCODING_H + VALUE new_encoding, old_encoding; + new_encoding = rb_funcall(cMysql2Client, intern_encoding_from_charset, 1, value); + if (new_encoding == Qnil) { + rb_raise(cMysql2Error, "Unsupported charset: '%s'", RSTRING_PTR(value)); + } else { + old_encoding = rb_iv_get(self, "@encoding"); + if (old_encoding == Qnil) { + rb_iv_set(self, "@encoding", new_encoding); + } + } +#endif + + charset_name = StringValuePtr(value); + + if (mysql_options(client, MYSQL_SET_CHARSET_NAME, charset_name)) { + /* TODO: warning - unable to set charset */ + rb_warn("%s\n", mysql_error(client)); + } + + return value; +} + +static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE capath, VALUE cipher) { + MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + + if(!NIL_P(ca) || !NIL_P(key)) { + mysql_ssl_set(client, + NIL_P(key) ? NULL : StringValuePtr(key), + NIL_P(cert) ? NULL : StringValuePtr(cert), + NIL_P(ca) ? NULL : StringValuePtr(ca), + NIL_P(capath) ? NULL : StringValuePtr(capath), + NIL_P(cipher) ? NULL : StringValuePtr(cipher)); + } + + return self; +} + +static VALUE init_connection(VALUE self) { + MYSQL * client; + Data_Get_Struct(self, MYSQL, client); + + if (rb_thread_blocking_region(nogvl_init, client, RUBY_UBF_IO, 0) == Qfalse) { + /* TODO: warning - not enough memory? */ + return rb_raise_mysql2_error(client); + } + + return self; +} + +void init_mysql2_client() { + cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject); + + rb_define_alloc_func(cMysql2Client, allocate); + + rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0); + rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1); + rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1); + rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0); + rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0); + rb_define_method(cMysql2Client, "socket", rb_mysql_client_socket, 0); + rb_define_method(cMysql2Client, "async_result", rb_mysql_client_async_result, 0); + rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0); + rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0); + + rb_define_private_method(cMysql2Client, "reconnect=", set_reconnect, 1); + rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1); + rb_define_private_method(cMysql2Client, "charset_name=", set_charset_name, 1); + 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); +} \ No newline at end of file diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h new file mode 100644 index 0000000..a962303 --- /dev/null +++ b/ext/mysql2/client.h @@ -0,0 +1,34 @@ +#ifndef MYSQL2_CLIENT_H +#define MYSQL2_CLIENT_H + +/* + * partial emulation of the 1.9 rb_thread_blocking_region under 1.8, + * this is enough for dealing with blocking I/O functions in the + * presence of threads. + */ +#ifndef HAVE_RB_THREAD_BLOCKING_REGION + +#include +#define RUBY_UBF_IO ((rb_unblock_function_t *)-1) +typedef void rb_unblock_function_t(void *); +typedef VALUE rb_blocking_function_t(void *); +static VALUE +rb_thread_blocking_region( + rb_blocking_function_t *func, void *data1, + RB_MYSQL_UNUSED rb_unblock_function_t *ubf, + RB_MYSQL_UNUSED void *data2) +{ + VALUE rv; + + TRAP_BEG; + rv = func(data1); + TRAP_END; + + return rv; +} + +#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */ + +void init_mysql2_client(); + +#endif \ No newline at end of file diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 3441b04..68e276b 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -1,497 +1,19 @@ #include -VALUE mMysql2, cMysql2Client; -VALUE cMysql2Error, intern_encoding_from_charset; -ID sym_id, sym_version, sym_async; - -#define REQUIRE_OPEN_DB(_ctxt) \ - if(!_ctxt->net.vio) { \ - rb_raise(cMysql2Error, "closed MySQL connection"); \ - return Qnil; \ - } - -/* - * non-blocking mysql_*() functions that we won't be wrapping since - * they do not appear to hit the network nor issue any interruptible - * or blocking system calls. - * - * - mysql_affected_rows() - * - mysql_error() - * - mysql_fetch_fields() - * - mysql_fetch_lengths() - calls cli_fetch_lengths or emb_fetch_lengths - * - mysql_field_count() - * - mysql_get_client_info() - * - mysql_get_client_version() - * - mysql_get_server_info() - * - mysql_get_server_version() - * - mysql_insert_id() - * - mysql_num_fields() - * - mysql_num_rows() - * - mysql_options() - * - mysql_real_escape_string() - * - mysql_ssl_set() - */ - -static VALUE rb_raise_mysql2_error(MYSQL *client) { - VALUE e = rb_exc_new2(cMysql2Error, mysql_error(client)); - rb_funcall(e, rb_intern("error_number="), 1, INT2NUM(mysql_errno(client))); - rb_funcall(e, rb_intern("sql_state="), 1, rb_tainted_str_new2(mysql_sqlstate(client))); - rb_exc_raise(e); - return Qnil; -} - -static VALUE nogvl_init(void *ptr) { - MYSQL * client = (MYSQL *)ptr; - - /* may initialize embedded server and read /etc/services off disk */ - mysql_init(client); - - return client ? Qtrue : Qfalse; -} - -static VALUE nogvl_connect(void *ptr) { - struct nogvl_connect_args *args = ptr; - MYSQL *client; - - client = mysql_real_connect(args->mysql, args->host, - args->user, args->passwd, - args->db, args->port, args->unix_socket, - args->client_flag); - - return client ? Qtrue : Qfalse; -} - -static void rb_mysql_client_free(void * ptr) { - MYSQL * client = (MYSQL *)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; - - 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); - if (flags > 0 && !(flags & O_NONBLOCK)) - fcntl(fd, F_SETFL, flags | O_NONBLOCK); - } - - /* It's safe to call mysql_close() on an already closed connection. */ - mysql_close(client); - xfree(ptr); -} - -static VALUE nogvl_close(void * ptr) { - MYSQL *client = (MYSQL *)ptr; - mysql_close(client); - client->net.fd = -1; - return Qnil; -} - -static VALUE allocate(VALUE klass) -{ - MYSQL * client; - - return Data_Make_Struct( - klass, - MYSQL, - NULL, - rb_mysql_client_free, - client - ); -} - -static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket) -{ - MYSQL * client; - struct nogvl_connect_args args; - - Data_Get_Struct(self, MYSQL, client); - - args.host = NIL_P(host) ? "localhost" : StringValuePtr(host); - args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket); - args.port = NIL_P(port) ? 3306 : NUM2INT(port); - 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.client_flag = 0; - - if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) - { - // unable to connect - return rb_raise_mysql2_error(client); - } - - return self; -} - -/* - * Immediately disconnect from the server, normally the garbage collector - * will disconnect automatically when a connection is no longer needed. - * Explicitly closing this will free up server resources sooner than waiting - * for the garbage collector. - */ -static VALUE rb_mysql_client_close(VALUE self) { - MYSQL *client; - - Data_Get_Struct(self, MYSQL, client); - - rb_thread_blocking_region(nogvl_close, client, RUBY_UBF_IO, 0); - - return Qnil; -} - -/* - * mysql_send_query is unlikely to block since most queries are small - * enough to fit in a socket buffer, but sometimes large UPDATE and - * INSERTs will cause the process to block - */ -static VALUE nogvl_send_query(void *ptr) { - struct nogvl_send_query_args *args = ptr; - int rv; - const char *sql = StringValuePtr(args->sql); - long sql_len = RSTRING_LEN(args->sql); - - rv = mysql_send_query(args->mysql, sql, sql_len); - - return rv == 0 ? Qtrue : Qfalse; -} - -/* - * even though we did rb_thread_select before calling this, a large - * response can overflow the socket buffers and cause us to eventually - * block while calling mysql_read_query_result - */ -static VALUE nogvl_read_query_result(void *ptr) { - MYSQL * client = ptr; - my_bool res = mysql_read_query_result(client); - - return res == 0 ? Qtrue : Qfalse; -} - -/* mysql_store_result may (unlikely) read rows off the socket */ -static VALUE nogvl_store_result(void *ptr) { - MYSQL * client = ptr; - return (VALUE)mysql_store_result(client); -} - -static VALUE rb_mysql_client_async_result(VALUE self) { - MYSQL * client; - MYSQL_RES * result; - - Data_Get_Struct(self, MYSQL, client); - - REQUIRE_OPEN_DB(client); - if (rb_thread_blocking_region(nogvl_read_query_result, client, RUBY_UBF_IO, 0) == Qfalse) { - return rb_raise_mysql2_error(client); - } - - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, client, RUBY_UBF_IO, 0); - if (result == NULL) { - if (mysql_field_count(client) != 0) { - rb_raise_mysql2_error(client); - } - return Qnil; - } - - VALUE resultObj = rb_mysql_result_to_obj(result); - rb_iv_set(resultObj, "@encoding", rb_iv_get(self, "@encoding")); - return resultObj; -} - -static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { - struct nogvl_send_query_args args; - fd_set fdset; - int fd, retval; - int async = 0; - VALUE opts; - VALUE rb_async; - - MYSQL * client; - - 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; - } - } - - 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); - } - - if (!async) { - // the below code is largely from do_mysql - // http://github.com/datamapper/do - fd = client->net.fd; - for(;;) { - FD_ZERO(&fdset); - FD_SET(fd, &fdset); - - retval = rb_thread_select(fd + 1, &fdset, NULL, NULL, NULL); - - if (retval < 0) { - rb_sys_fail(0); - } - - if (retval > 0) { - break; - } - } - - return rb_mysql_client_async_result(self); - } else { - return Qnil; - } -} - -static VALUE rb_mysql_client_escape(VALUE self, VALUE str) { - MYSQL * client; - VALUE newStr; - unsigned long newLen, oldLen; - - Check_Type(str, T_STRING); -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *default_internal_enc = rb_default_internal_encoding(); - rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); - // ensure the string is in the encoding the connection is expecting - str = rb_str_export_to_enc(str, conn_enc); -#endif - - oldLen = RSTRING_LEN(str); - char escaped[(oldLen*2)+1]; - - Data_Get_Struct(self, MYSQL, client); - - REQUIRE_OPEN_DB(client); - newLen = mysql_real_escape_string(client, escaped, StringValuePtr(str), RSTRING_LEN(str)); - if (newLen == oldLen) { - // no need to return a new ruby string if nothing changed - return str; - } else { - newStr = rb_str_new(escaped, newLen); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(newStr, conn_enc); - if (default_internal_enc) { - newStr = rb_str_export_to_enc(newStr, default_internal_enc); - } -#endif - return newStr; - } -} - -static VALUE rb_mysql_client_info(RB_MYSQL_UNUSED VALUE self) { - VALUE version = rb_hash_new(), client_info; -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *default_internal_enc = rb_default_internal_encoding(); - rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); -#endif - - rb_hash_aset(version, sym_id, LONG2NUM(mysql_get_client_version())); - client_info = rb_str_new2(mysql_get_client_info()); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(client_info, conn_enc); - if (default_internal_enc) { - client_info = rb_str_export_to_enc(client_info, default_internal_enc); - } -#endif - rb_hash_aset(version, sym_version, client_info); - return version; -} - -static VALUE rb_mysql_client_server_info(VALUE self) { - MYSQL * client; - VALUE version, server_info; -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *default_internal_enc = rb_default_internal_encoding(); - rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); -#endif - - Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); - - version = rb_hash_new(); - rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(client))); - server_info = rb_str_new2(mysql_get_server_info(client)); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(server_info, conn_enc); - if (default_internal_enc) { - server_info = rb_str_export_to_enc(server_info, default_internal_enc); - } -#endif - rb_hash_aset(version, sym_version, server_info); - return version; -} - -static VALUE rb_mysql_client_socket(VALUE self) { - MYSQL * client; - Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); - return INT2NUM(client->net.fd); -} - -static VALUE rb_mysql_client_last_id(VALUE self) { - MYSQL * client; - Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); - return ULL2NUM(mysql_insert_id(client)); -} - -static VALUE rb_mysql_client_affected_rows(VALUE self) { - MYSQL * client; - Data_Get_Struct(self, MYSQL, client); - REQUIRE_OPEN_DB(client); - return ULL2NUM(mysql_affected_rows(client)); -} - -static VALUE set_reconnect(VALUE self, VALUE value) -{ - my_bool reconnect; - MYSQL * client; - - Data_Get_Struct(self, MYSQL, client); - - if(!NIL_P(value)) { - reconnect = value == Qfalse ? 0 : 1; - - /* set default reconnect behavior */ - if (mysql_options(client, MYSQL_OPT_RECONNECT, &reconnect)) { - /* TODO: warning - unable to set reconnect behavior */ - rb_warn("%s\n", mysql_error(client)); - } - } - return value; -} - -static VALUE set_connect_timeout(VALUE self, VALUE value) -{ - unsigned int connect_timeout = 0; - MYSQL * client; - - Data_Get_Struct(self, MYSQL, client); - - 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)) { - /* TODO: warning - unable to set connection timeout */ - rb_warn("%s\n", mysql_error(client)); - } - } - return value; -} - -static VALUE set_charset_name(VALUE self, VALUE value) -{ - char * charset_name; - MYSQL * client; - - Data_Get_Struct(self, MYSQL, client); - -#ifdef HAVE_RUBY_ENCODING_H - VALUE new_encoding, old_encoding; - new_encoding = rb_funcall(cMysql2Client, intern_encoding_from_charset, 1, value); - if (new_encoding == Qnil) { - rb_raise(cMysql2Error, "Unsupported charset: '%s'", RSTRING_PTR(value)); - } else { - old_encoding = rb_iv_get(self, "@encoding"); - if (old_encoding == Qnil) { - rb_iv_set(self, "@encoding", new_encoding); - } - } -#endif - - charset_name = StringValuePtr(value); - - if (mysql_options(client, MYSQL_SET_CHARSET_NAME, charset_name)) { - /* TODO: warning - unable to set charset */ - rb_warn("%s\n", mysql_error(client)); - } - - return value; -} - -static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE capath, VALUE cipher) -{ - MYSQL * client; - Data_Get_Struct(self, MYSQL, client); - - if(!NIL_P(ca) || !NIL_P(key)) { - mysql_ssl_set(client, - NIL_P(key) ? NULL : StringValuePtr(key), - NIL_P(cert) ? NULL : StringValuePtr(cert), - NIL_P(ca) ? NULL : StringValuePtr(ca), - NIL_P(capath) ? NULL : StringValuePtr(capath), - NIL_P(cipher) ? NULL : StringValuePtr(cipher)); - } - - return self; -} - -static VALUE init_connection(VALUE self) -{ - MYSQL * client; - Data_Get_Struct(self, MYSQL, client); - - if (rb_thread_blocking_region(nogvl_init, client, RUBY_UBF_IO, 0) == Qfalse) { - /* TODO: warning - not enough memory? */ - return rb_raise_mysql2_error(client); - } - - return self; -} +VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; +ID sym_id, sym_version, sym_async; /* Ruby Extension initializer */ void Init_mysql2() { - mMysql2 = rb_define_module("Mysql2"); - cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject); - - rb_define_alloc_func(cMysql2Client, allocate); - - rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0); - rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1); - rb_define_method(cMysql2Client, "escape", rb_mysql_client_escape, 1); - rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0); - rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0); - rb_define_method(cMysql2Client, "socket", rb_mysql_client_socket, 0); - rb_define_method(cMysql2Client, "async_result", rb_mysql_client_async_result, 0); - rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0); - rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0); - - rb_define_private_method(cMysql2Client, "reconnect=", set_reconnect, 1); - rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1); - rb_define_private_method(cMysql2Client, "charset_name=", set_charset_name, 1); - 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); - + mMysql2 = rb_define_module("Mysql2"); cMysql2Error = rb_const_get(mMysql2, rb_intern("Error")); - init_mysql2_result(); - - sym_id = ID2SYM(rb_intern("id")); - sym_version = ID2SYM(rb_intern("version")); - sym_async = ID2SYM(rb_intern("async")); + sym_id = ID2SYM(rb_intern("id")); + sym_version = ID2SYM(rb_intern("version")); + sym_async = ID2SYM(rb_intern("async")); intern_encoding_from_charset = rb_intern("encoding_from_charset"); + + init_mysql2_client(); + init_mysql2_result(); } diff --git a/ext/mysql2/mysql2_ext.h b/ext/mysql2/mysql2_ext.h index 0c18884..e01e092 100644 --- a/ext/mysql2/mysql2_ext.h +++ b/ext/mysql2/mysql2_ext.h @@ -26,73 +26,7 @@ #define RB_MYSQL_UNUSED #endif +#include #include -extern VALUE mMysql2; - -/* Mysql2::Error */ -extern VALUE cMysql2Error; - -/* Mysql2::Result */ -typedef struct { - VALUE fields; - VALUE rows; - unsigned int numberOfFields; - unsigned long numberOfRows; - unsigned long lastRowProcessed; - short int resultFreed; - MYSQL_RES *result; -} mysql2_result_wrapper; -#define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj)); - -/* - * used to pass all arguments to mysql_real_connect while inside - * rb_thread_blocking_region - */ -struct nogvl_connect_args { - MYSQL *mysql; - const char *host; - const char *user; - const char *passwd; - const char *db; - unsigned int port; - const char *unix_socket; - unsigned long client_flag; -}; - -/* - * used to pass all arguments to mysql_send_query while inside - * rb_thread_blocking_region - */ -struct nogvl_send_query_args { - MYSQL *mysql; - VALUE sql; -}; - -/* - * partial emulation of the 1.9 rb_thread_blocking_region under 1.8, - * this is enough for dealing with blocking I/O functions in the - * presence of threads. - */ -#ifndef HAVE_RB_THREAD_BLOCKING_REGION -# include -# define RUBY_UBF_IO ((rb_unblock_function_t *)-1) -typedef void rb_unblock_function_t(void *); -typedef VALUE rb_blocking_function_t(void *); -static VALUE -rb_thread_blocking_region( - rb_blocking_function_t *func, void *data1, - RB_MYSQL_UNUSED rb_unblock_function_t *ubf, - RB_MYSQL_UNUSED void *data2) -{ - VALUE rv; - - TRAP_BEG; - rv = func(data1); - TRAP_END; - - return rv; -} -#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */ - #endif diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 5b12b87..6a2df6c 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -7,16 +7,16 @@ rb_encoding *binaryEncoding; ID sym_symbolize_keys; ID intern_new, intern_utc, intern_encoding_from_charset_code; -VALUE cBigDecimal, cDate, cDateTime; VALUE cMysql2Result; -extern VALUE cMysql2Client; +VALUE cBigDecimal, cDate, cDateTime; +extern VALUE mMysql2, cMysql2Client, cMysql2Error, intern_encoding_from_charset; static void rb_mysql_result_mark(void * wrapper) { - mysql2_result_wrapper * w = wrapper; - if (w) { - rb_gc_mark(w->fields); - rb_gc_mark(w->rows); - } + mysql2_result_wrapper * w = wrapper; + if (w) { + rb_gc_mark(w->fields); + rb_gc_mark(w->rows); + } } /* this may be called manually or during GC */ diff --git a/ext/mysql2/result.h b/ext/mysql2/result.h index 3b9eeab..c466a7c 100644 --- a/ext/mysql2/result.h +++ b/ext/mysql2/result.h @@ -4,4 +4,16 @@ void init_mysql2_result(); VALUE rb_mysql_result_to_obj(MYSQL_RES * r); +typedef struct { + VALUE fields; + VALUE rows; + unsigned int numberOfFields; + unsigned long numberOfRows; + unsigned long lastRowProcessed; + short int resultFreed; + MYSQL_RES *result; +} mysql2_result_wrapper; + +#define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj)); + #endif