diff --git a/CHANGELOG.md b/CHANGELOG.md index f90d6f1..42d5ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +# 0.2.2 (August 19th, 2010) +* Change how AR adapter would send initial commands upon connecting +** we can make multiple session variable assignments in a single query +* fix signal handling when waiting on queries +* retry connect if interrupted by signals + +## 0.2.1 (August 16th, 2010) +* bring mysql2 ActiveRecord adapter back into gem + +## 0.2.0 (August 16th, 2010) +* switch back to letting libmysql manage all allocation/thread-state/freeing for the connection +* cache various numeric type conversions in hot-spots of the code for a little speed boost +* ActiveRecord adapter moved into Rails 3 core +** Don't worry 2.3.x users! We'll either release the adapter as a separate gem, or try to get it into 2.3.9 +* Fix for the "closed MySQL connection" error (GH #31) +* Fix for the "can't modify frozen object" error in 1.9.2 (GH #37) +* Introduce cascading query and result options (more info in README) +* Sequel adapter pulled into core (will be in the next release - 3.15.0 at the time of writing) +* add a safety check when attempting to send a query before a result has been fetched + ## 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 diff --git a/README.rdoc b/README.rdoc index 990b5cd..9398586 100644 --- a/README.rdoc +++ b/README.rdoc @@ -102,7 +102,22 @@ If you'd like to see either of these (or others), open an issue and start buggin == Timezones -You can set the :timezone option to :local or :utc to tell Mysql2 which timezone you'd like to have Time objects in +Mysql2 now supports two timezone options: + + :database_timezone - this is the timezone Mysql2 will assume fields are already stored as, and will use this when creating the initial Time objects in ruby + :application_timezone - this is the timezone Mysql2 will convert to before finally handing back to the caller + +In other words, if :database_timezone is set to :utc - Mysql2 will create the Time objects using Time.utc(...) from the raw value libmysql hands over initially. +Then, if :application_timezone is set to say - :local - Mysql2 will then convert the just-created UTC Time object to local time. + +Both options only allow two values - :local or :utc - with the exception that :application_timezone can be [and defaults to] nil + +== Casting "boolean" columns + +You can now tell Mysql2 to cast tinyint(1) fields to boolean values in Ruby with the :cast_booleans option. + + client = Mysql2::Client.new + result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true) == Async @@ -131,6 +146,10 @@ That was easy right? :) You can also use Mysql2 with asynchronous Rails (first introduced at http://www.mikeperham.com/2010/04/03/introducing-phat-an-asynchronous-rails-app/) by setting the adapter in your database.yml to "em_mysql2". You must be running Ruby 1.9, thin and the rack-fiber_pool middleware for it to work. +== Sequel + +The Sequel adapter was pulled out into Sequel core (will be part of the next release) and can be used by specifying the "mysql2://" prefix to your connection specification. + == EventMachine The mysql2 EventMachine deferrable api allows you to make async queries using EventMachine, diff --git a/Rakefile b/Rakefile index 6c3e3c3..6c39ead 100644 --- a/Rakefile +++ b/Rakefile @@ -33,6 +33,8 @@ end Spec::Rake::SpecTask.new('spec') do |t| t.spec_files = FileList['spec/'] t.spec_opts << '--options' << 'spec/spec.opts' + t.verbose = true + t.warning = true end Spec::Rake::SpecTask.new('spec:gdb') do |t| diff --git a/VERSION b/VERSION index 1a03094..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.9 +0.2.2 diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 51ba1f3..8181625 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -1,5 +1,6 @@ #include #include +#include VALUE cMysql2Client; extern VALUE mMysql2, cMysql2Error; @@ -96,10 +97,12 @@ 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); + do { + client = mysql_real_connect(args->mysql, args->host, + args->user, args->passwd, + args->db, args->port, args->unix_socket, + args->client_flag); + } while (! client && errno == EINTR && (errno = 0) == 0); return client ? Qtrue : Qfalse; } @@ -147,7 +150,7 @@ static VALUE allocate(VALUE klass) { return obj; } -static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket) { +static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket, VALUE flags) { struct nogvl_connect_args args; GET_CLIENT(self) @@ -158,7 +161,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po args.passwd = NIL_P(pass) ? NULL : StringValuePtr(pass); args.db = NIL_P(database) ? NULL : StringValuePtr(database); args.mysql = client; - args.client_flag = 0; + args.client_flag = NUM2INT(flags); if (rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0) == Qfalse) { // unable to connect @@ -257,7 +260,6 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { int fd, retval; int async = 0; VALUE opts, defaults; - int(*selector)(int, fd_set *, fd_set *, fd_set *, struct timeval *) = NULL; GET_CLIENT(self) REQUIRE_OPEN_DB(client); @@ -299,12 +301,11 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { // the below code is largely from do_mysql // http://github.com/datamapper/do fd = client->net.fd; - selector = rb_thread_alone() ? *select : *rb_thread_select; for(;;) { FD_ZERO(&fdset); FD_SET(fd, &fdset); - retval = selector(fd + 1, &fdset, NULL, NULL, NULL); + retval = rb_thread_select(fd + 1, &fdset, NULL, NULL, NULL); if (retval < 0) { rb_sys_fail(0); @@ -537,7 +538,7 @@ void init_mysql2_client() { 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); + rb_define_private_method(cMysql2Client, "connect", rb_connect, 7); intern_encoding_from_charset = rb_intern("encoding_from_charset"); @@ -552,4 +553,109 @@ void init_mysql2_client() { intern_error_number_eql = rb_intern("error_number="); intern_sql_state_eql = rb_intern("sql_state="); -} \ No newline at end of file +#ifdef CLIENT_LONG_PASSWORD + rb_const_set(cMysql2Client, rb_intern("LONG_PASSWORD"), + INT2NUM(CLIENT_LONG_PASSWORD)); +#endif + +#ifdef CLIENT_FOUND_ROWS + rb_const_set(cMysql2Client, rb_intern("FOUND_ROWS"), + INT2NUM(CLIENT_FOUND_ROWS)); +#endif + +#ifdef CLIENT_LONG_FLAG + rb_const_set(cMysql2Client, rb_intern("LONG_FLAG"), + INT2NUM(CLIENT_LONG_FLAG)); +#endif + +#ifdef CLIENT_CONNECT_WITH_DB + rb_const_set(cMysql2Client, rb_intern("CONNECT_WITH_DB"), + INT2NUM(CLIENT_CONNECT_WITH_DB)); +#endif + +#ifdef CLIENT_NO_SCHEMA + rb_const_set(cMysql2Client, rb_intern("NO_SCHEMA"), + INT2NUM(CLIENT_NO_SCHEMA)); +#endif + +#ifdef CLIENT_COMPRESS + rb_const_set(cMysql2Client, rb_intern("COMPRESS"), INT2NUM(CLIENT_COMPRESS)); +#endif + +#ifdef CLIENT_ODBC + rb_const_set(cMysql2Client, rb_intern("ODBC"), INT2NUM(CLIENT_ODBC)); +#endif + +#ifdef CLIENT_LOCAL_FILES + rb_const_set(cMysql2Client, rb_intern("LOCAL_FILES"), + INT2NUM(CLIENT_LOCAL_FILES)); +#endif + +#ifdef CLIENT_IGNORE_SPACE + rb_const_set(cMysql2Client, rb_intern("IGNORE_SPACE"), + INT2NUM(CLIENT_IGNORE_SPACE)); +#endif + +#ifdef CLIENT_PROTOCOL_41 + rb_const_set(cMysql2Client, rb_intern("PROTOCOL_41"), + INT2NUM(CLIENT_PROTOCOL_41)); +#endif + +#ifdef CLIENT_INTERACTIVE + rb_const_set(cMysql2Client, rb_intern("INTERACTIVE"), + INT2NUM(CLIENT_INTERACTIVE)); +#endif + +#ifdef CLIENT_SSL + rb_const_set(cMysql2Client, rb_intern("SSL"), INT2NUM(CLIENT_SSL)); +#endif + +#ifdef CLIENT_IGNORE_SIGPIPE + rb_const_set(cMysql2Client, rb_intern("IGNORE_SIGPIPE"), + INT2NUM(CLIENT_IGNORE_SIGPIPE)); +#endif + +#ifdef CLIENT_TRANSACTIONS + rb_const_set(cMysql2Client, rb_intern("TRANSACTIONS"), + INT2NUM(CLIENT_TRANSACTIONS)); +#endif + +#ifdef CLIENT_RESERVED + rb_const_set(cMysql2Client, rb_intern("RESERVED"), INT2NUM(CLIENT_RESERVED)); +#endif + +#ifdef CLIENT_SECURE_CONNECTION + rb_const_set(cMysql2Client, rb_intern("SECURE_CONNECTION"), + INT2NUM(CLIENT_SECURE_CONNECTION)); +#endif + +#ifdef CLIENT_MULTI_STATEMENTS + rb_const_set(cMysql2Client, rb_intern("MULTI_STATEMENTS"), + INT2NUM(CLIENT_MULTI_STATEMENTS)); +#endif + +#ifdef CLIENT_PS_MULTI_RESULTS + rb_const_set(cMysql2Client, rb_intern("PS_MULTI_RESULTS"), + INT2NUM(CLIENT_PS_MULTI_RESULTS)); +#endif + +#ifdef CLIENT_SSL_VERIFY_SERVER_CERT + rb_const_set(cMysql2Client, rb_intern("SSL_VERIFY_SERVER_CERT"), + INT2NUM(CLIENT_SSL_VERIFY_SERVER_CERT)); +#endif + +#ifdef CLIENT_REMEMBER_OPTIONS + rb_const_set(cMysql2Client, rb_intern("REMEMBER_OPTIONS"), + INT2NUM(CLIENT_REMEMBER_OPTIONS)); +#endif + +#ifdef CLIENT_ALL_FLAGS + rb_const_set(cMysql2Client, rb_intern("ALL_FLAGS"), + INT2NUM(CLIENT_ALL_FLAGS)); +#endif + +#ifdef CLIENT_BASIC_FLAGS + rb_const_set(cMysql2Client, rb_intern("BASIC_FLAGS"), + INT2NUM(CLIENT_BASIC_FLAGS)); +#endif +} diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb new file mode 100644 index 0000000..b5ee9cd --- /dev/null +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1,641 @@ +# encoding: utf-8 + +require 'mysql2' unless defined? Mysql2 + +module ActiveRecord + class Base + def self.mysql2_connection(config) + config[:username] = 'root' if config[:username].nil? + client = Mysql2::Client.new(config.symbolize_keys) + options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] + ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + end + end + + module ConnectionAdapters + class Mysql2Column < Column + BOOL = "tinyint(1)" + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime then Time + when :date then Date + when :timestamp then Time + when :time then Time + when :text, :string then String + when :binary then String + when :boolean then Object + end + end + + def type_cast(value) + return nil if value.nil? + case type + when :string then value + when :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f # returns self if it's already a Float + when :decimal then self.class.value_to_decimal(value) + when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value) + when :time then value.class == Time ? value : self.class.string_to_dummy_time(value) + when :date then value.class == Date ? value : self.class.string_to_date(value) + when :binary then value + when :boolean then self.class.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + case type + when :string then nil + when :text then nil + when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0" + when :float then "#{var_name}.to_f" + when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" + when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" + when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" + when :binary then nil + when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" + else nil + end + end + + private + def simplified_type(field_type) + return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) + 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 + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + class Mysql2Adapter < AbstractAdapter + cattr_accessor :emulate_booleans + self.emulate_booleans = true + + ADAPTER_NAME = 'Mysql2' + PRIMARY = "PRIMARY" + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + configure_connection + end + + def adapter_name + ADAPTER_NAME + end + + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + def supports_savepoints? + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quote_string(string) + @connection.escape(string) + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # CONNECTION MANAGEMENT ==================================== + + def active? + return false unless @connection + @connection.query 'select 1' + true + rescue Mysql2::Error + false + end + + def reconnect! + disconnect! + 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 + @connection = nil + end + end + + def reset! + disconnect! + connect + end + + # DATABASE STATEMENTS ====================================== + + # 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+. + def select_rows(sql, name = nil) + execute(sql, name).to_a + end + + # 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[:database_timezone] = ActiveRecord::Base.default_timezone + if name == :skip_logging + @connection.query(sql) + else + log(sql, name) { @connection.query(sql) } + end + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + super + id_value || @connection.last_id + end + alias :create :insert_sql + + def update_sql(sql, name = nil) + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def add_limit_offset!(sql, options) + limit, offset = options[:limit], options[:offset] + if limit && offset + sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" + elsif limit + sql << " LIMIT #{sanitize_limit(limit)}" + elsif offset + sql << " OFFSET #{offset.to_i}" + end + sql + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name, options = {}) + drop_database(name) + create_database(name, options) + end + + # Create a new MySQL database with optional :charset and :collation. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil) + tables = [] + execute("SHOW TABLES", name).each do |field| + tables << field.first + end + tables + end + + def drop_table(table_name, options = {}) + super(table_name, options) + end + + def indexes(table_name, name = nil) + indexes = [] + current_index = nil + result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) + 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] + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, []) + end + + indexes.last.columns << row[:Column_name] + end + indexes + end + + def columns(table_name, name = nil) + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + columns = [] + result = execute(sql, :skip_logging) + result.each(:symbolize_keys => true, :as => :hash) { |field| + columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + } + columns + end + + def create_table(table_name, options = {}) + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def rename_table(table_name, new_name) + execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + def show_variable(name) + variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables.first['Value'] unless variables.empty? + end + + def pk_and_sequence_for(table) + keys = [] + result = execute("describe #{quote_table_name(table)}") + result.each(:symbolize_keys => true, :as => :hash) do |row| + keys << row[:Field] if row[:Key] == "PRI" + end + keys.length == 1 ? [keys.first, nil] : nil + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_equality_operator + "= BINARY" + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + quoted_column_names = case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + return super unless exception.respond_to?(:error_number) + + case exception.error_number + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + end + + private + def connect + @connection = Mysql2::Client.new(@config) + configure_connection + end + + def configure_connection + @connection.query_options.merge!(:as => :array) + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] + variable_assignments << "NAMES '#{encoding}'" if encoding + + execute("SET #{variable_assignments.join(', ')}", :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).each(:as => :hash) + end + + def supports_views? + version[0] >= 5 + end + + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + end + end +end diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 1d0ef4b..6bdadb6 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -12,5 +12,5 @@ require 'mysql2/field' # # A modern, simple and very fast Mysql library for Ruby - binding to libmysql module Mysql2 - VERSION = "0.1.9" + VERSION = "0.2.2" end diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index 81d08bc..cbb6a60 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -30,8 +30,9 @@ module Mysql2 port = opts[:port] || 3306 database = opts[:database] socket = opts[:socket] + flags = opts[:flags] || 0 - connect user, pass, host, port, database, socket + connect user, pass, host, port, database, socket, flags end def self.default_query_options diff --git a/mysql2.gemspec b/mysql2.gemspec index 4fac216..f84097f 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{mysql2} - s.version = "0.1.9" + s.version = "0.2.2" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Brian Lopez"] - s.date = %q{2010-08-01} + s.date = %q{2010-08-19} s.email = %q{seniorlopez@gmail.com} s.extensions = ["ext/mysql2/extconf.rb"] s.extra_rdoc_files = [ @@ -23,12 +23,15 @@ Gem::Specification.new do |s| "Rakefile", "VERSION", "benchmark/active_record.rb", + "benchmark/allocations.rb", "benchmark/escape.rb", "benchmark/query_with_mysql_casting.rb", "benchmark/query_without_mysql_casting.rb", "benchmark/sequel.rb", "benchmark/setup_db.rb", + "benchmark/thread_alone.rb", "examples/eventmachine.rb", + "examples/threaded.rb", "ext/mysql2/client.c", "ext/mysql2/client.h", "ext/mysql2/extconf.rb", @@ -37,6 +40,7 @@ Gem::Specification.new do |s| "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", @@ -51,7 +55,10 @@ Gem::Specification.new do |s| "spec/mysql2/result_spec.rb", "spec/rcov.opts", "spec/spec.opts", - "spec/spec_helper.rb" + "spec/spec_helper.rb", + "tasks/benchmarks.rake", + "tasks/compile.rake", + "tasks/vendor_mysql.rake" ] s.homepage = %q{http://github.com/brianmario/mysql2} s.rdoc_options = ["--charset=UTF-8"] @@ -64,7 +71,8 @@ Gem::Specification.new do |s| "spec/mysql2/error_spec.rb", "spec/mysql2/result_spec.rb", "spec/spec_helper.rb", - "examples/eventmachine.rb" + "examples/eventmachine.rb", + "examples/threaded.rb" ] if s.respond_to? :specification_version then diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 7c5c8ce..b3ffcbc 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -14,6 +14,30 @@ describe Mysql2::Client do end end + it "should accept connect flags and pass them to #connect" do + klient = Class.new(Mysql2::Client) do + attr_reader :connect_args + def connect *args + @connect_args ||= [] + @connect_args << args + end + end + client = klient.new :flags => Mysql2::Client::FOUND_ROWS + client.connect_args.last.last.should == Mysql2::Client::FOUND_ROWS + end + + it "should default flags to 0" do + klient = Class.new(Mysql2::Client) do + attr_reader :connect_args + def connect *args + @connect_args ||= [] + @connect_args << args + end + end + client = klient.new + client.connect_args.last.last.should == 0 + end + it "should have a global default_query_options hash" do Mysql2::Client.should respond_to(:default_query_options) end @@ -78,6 +102,33 @@ describe Mysql2::Client do @client.query("SELECT 1") }.should raise_error(Mysql2::Error) end + + # XXX this test is not deterministic (because Unix signal handling is not) + # and may fail on a loaded system + it "should run signal handlers while waiting for a response" do + mark = {} + trap(:USR1) { mark[:USR1] = Time.now } + begin + mark[:START] = Time.now + pid = fork do + sleep 1 # wait for client "SELECT sleep(2)" query to start + Process.kill(:USR1, Process.ppid) + sleep # wait for explicit kill to prevent GC disconnect + end + @client.query("SELECT sleep(2)") + mark[:END] = Time.now + mark.include?(:USR1).should be_true + (mark[:USR1] - mark[:START]).should >= 1 + (mark[:USR1] - mark[:START]).should < 1.1 + (mark[:END] - mark[:USR1]).should > 0.9 + (mark[:END] - mark[:START]).should >= 2 + (mark[:END] - mark[:START]).should < 2.1 + Process.kill(:TERM, pid) + Process.waitpid2(pid) + ensure + trap(:USR1, 'DEFAULT') + end + end if RUBY_PLATFORM !~ /mingw|mswin/ end it "should respond to #escape" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 49c3b78..39da1e4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,6 @@ # encoding: UTF-8 -$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/..') -$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') +require 'rubygems' require 'mysql2' require 'timeout' @@ -64,4 +63,4 @@ Spec::Runner.configure do |config| ) ] end -end \ No newline at end of file +end