From ed4841e29ce37a8334dadf5b9f8579cdf4d37b4d Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 12:25:54 -0700 Subject: [PATCH 01/15] Version bump to 0.2.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1a03094..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.9 +0.2.0 From 91671e0d9aa3eed0011c5597807169ce565c0a34 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 12:38:08 -0700 Subject: [PATCH 02/15] update files for 0.2.0 release --- CHANGELOG.md | 11 +++++++++++ README.rdoc | 24 +++++++++++++++++++++--- lib/mysql2.rb | 2 +- mysql2.gemspec | 15 +++++++++++---- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f90d6f1..0ee7689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 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 #34) +* 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..ad7ef85 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 @@ -123,14 +138,17 @@ If you need multiple query concurrency take a look at using a connection pool. == ActiveRecord -To use the ActiveRecord driver, all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2". -That was easy right? :) +The ActiveRecord adapter has been removed from the mysql2 gem and is now part of Rails 3 core. We'll be releasing a separate gem or trying to get it into 2.3.9 for 2.3.x users. == Asynchronous ActiveRecord 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/lib/mysql2.rb b/lib/mysql2.rb index fb24cfc..8244d0c 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.9" + VERSION = "0.2.0" end diff --git a/mysql2.gemspec b/mysql2.gemspec index 4fac216..ae28cbb 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.0" 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-16} 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", @@ -51,7 +54,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 +70,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 From 164ad073328aa74a6d86b203399a3acb5412eb36 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 12:39:53 -0700 Subject: [PATCH 03/15] referenced the wrong issue # --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee7689..c6111ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * 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 #34) +* 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) From 95fb97dc2bc61dbc14b0c455e554d0aec8367b00 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 14:43:48 -0700 Subject: [PATCH 04/15] add AR mysql2 adatper back --- .../connection_adapters/mysql2_adapter.rb | 639 ++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 lib/active_record/connection_adapters/mysql2_adapter.rb 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..96cf2d0 --- /dev/null +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1,639 @@ +# 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) + encoding = @config[:encoding] + execute("SET NAMES '#{encoding}'", :skip_logging) if encoding + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + end + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select(sql, name = nil) + execute(sql, name).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 From e87427abe406b446fa8236f686d83a40e807dbb0 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 14:45:26 -0700 Subject: [PATCH 05/15] change AR adapter instructions back --- README.rdoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index ad7ef85..9398586 100644 --- a/README.rdoc +++ b/README.rdoc @@ -138,7 +138,8 @@ If you need multiple query concurrency take a look at using a connection pool. == ActiveRecord -The ActiveRecord adapter has been removed from the mysql2 gem and is now part of Rails 3 core. We'll be releasing a separate gem or trying to get it into 2.3.9 for 2.3.x users. +To use the ActiveRecord driver, all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2". +That was easy right? :) == Asynchronous ActiveRecord From dd23e2c8800e609f7a94fdc5e1b83a860b71a52e Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 14:46:54 -0700 Subject: [PATCH 06/15] Version bump to 0.2.1 --- CHANGELOG.md | 3 +++ VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6111ea..2014855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 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 diff --git a/VERSION b/VERSION index 0ea3a94..0c62199 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 From d962ef2583aed459f04f7eb8f320d3802a1d837a Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Mon, 16 Aug 2010 14:47:17 -0700 Subject: [PATCH 07/15] update files for 0.2.1 release --- lib/mysql2.rb | 2 +- mysql2.gemspec | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 8244d0c..9f47b6b 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.2.0" + VERSION = "0.2.1" end diff --git a/mysql2.gemspec b/mysql2.gemspec index ae28cbb..2842380 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = %q{mysql2} - s.version = "0.2.0" + s.version = "0.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Brian Lopez"] @@ -40,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", From 60c33be87c291e6008b1d0cb4e9397d928902408 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Tue, 17 Aug 2010 22:55:31 -0700 Subject: [PATCH 08/15] multiple variable assignments can done in a single query --- lib/active_record/connection_adapters/mysql2_adapter.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 96cf2d0..1b433ae 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -606,12 +606,11 @@ module ActiveRecord def configure_connection @connection.query_options.merge!(:as => :array) - encoding = @config[:encoding] - execute("SET NAMES '#{encoding}'", :skip_logging) if encoding # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + encoding = @config[:encoding] + execute("SET NAMES '#{encoding}', SQL_AUTO_IS_NULL=0", :skip_logging) if encoding end # Returns an array of record hashes with the column names as keys and From d990f683201abc43b2c1d42a8e00164c291782a8 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Wed, 18 Aug 2010 12:46:39 -0700 Subject: [PATCH 09/15] slight refactor of how initial commands were being sent --- lib/active_record/connection_adapters/mysql2_adapter.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/mysql2_adapter.rb b/lib/active_record/connection_adapters/mysql2_adapter.rb index 1b433ae..b5ee9cd 100644 --- a/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -609,8 +609,11 @@ module ActiveRecord # 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] - execute("SET NAMES '#{encoding}', SQL_AUTO_IS_NULL=0", :skip_logging) if 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 From 8bfbfa2708abfc420827eaef0e817657d9ba9b9d Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 18 Aug 2010 19:06:05 -0700 Subject: [PATCH 10/15] fix signal handling when waiting on queries This reverts the single-threaded select() optimization from commits 9a63a587c01e4ac116ba79e9cc23a3b3fddf589d and 0190457dbd6d963de1a16a3c5165ad346a0571d8. Under Ruby 1.9, the reverted optimization caused signal handlers to be delayed until the socket became readable. Under Ruby 1.8, matters are worse as receiving a signal during select() causes Errno::EINTR to be raised. There is now a (somewhat fragile) spec for testing signal handling during a "SELECT sleep(2)" query. Furthermore, the performance difference I measured on benchmark/thread_alone.rb was negligible (over 1000 iterations) between the two: Ruby 1.8.7-p249 Rehearsal ---------------------------------------------------- select 0.040000 0.020000 0.060000 ( 0.119448) rb_thread_select 0.050000 0.020000 0.070000 ( 0.132091) ------------------------------------------- total: 0.130000sec user system total real select 0.030000 0.030000 0.060000 ( 0.116471) rb_thread_select 0.050000 0.020000 0.070000 ( 0.119874) Ruby 1.9.2-p0 Rehearsal ---------------------------------------------------- select 0.050000 0.030000 0.080000 ( 0.134208) rb_thread_select 0.080000 0.000000 0.080000 ( 0.141316) ------------------------------------------- total: 0.160000sec user system total real select 0.050000 0.020000 0.070000 ( 0.123325) rb_thread_select 0.060000 0.010000 0.070000 ( 0.124075) Benchmarks were performed on an _empty_ mysql2_test table to maximize the relative time for the select(2)-related code path (vs reading data). The test was run on Debian Lenny, x86_64 and a stock 2.6.35.2 Linux kernel. Hardware was a Core2 Duo @ 1.6GHz with the performance CPU governor while on AC power to eliminate CPU speed fluctuations during the test. --- ext/mysql2/client.c | 4 +--- spec/mysql2/client_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 2650b2f..f2942c2 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -257,7 +257,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 +298,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); diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 7c5c8ce..2df7a77 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -78,6 +78,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 From 8e81dcb053a63c7bdc26eaf746b466e92ebd9b92 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 19 Aug 2010 14:16:02 -0700 Subject: [PATCH 11/15] retry connect if interrupted by signals The MySQL client libraries normally retry system calls when interrupted by signals. The lone exception I've found is in the (infrequent) connection setup where it'll propagate the connect(2) syscall error all the way back up to the caller. Fortunately inspection of the MySQL client library reveals it properly preserves the global "errno" variable even with debugging enabled. Note that the net.last_errno member does NOT correspond to the system "errno". --- ext/mysql2/client.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index f2942c2..f4a43f9 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; } From 554837a9326f309994c3c6b9370ee1035fa8ee15 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 19 Aug 2010 16:03:29 -0700 Subject: [PATCH 12/15] Version bump to 0.2.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 From 4935931431baeea64ae4e0a1dedef6858d9a33fa Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Thu, 19 Aug 2010 16:03:43 -0700 Subject: [PATCH 13/15] update files for 0.2.2 release --- CHANGELOG.md | 6 ++++++ lib/mysql2.rb | 2 +- mysql2.gemspec | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2014855..42d5ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 9f47b6b..5446d2f 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.2.1" + VERSION = "0.2.2" end diff --git a/mysql2.gemspec b/mysql2.gemspec index 2842380..f84097f 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -5,11 +5,11 @@ Gem::Specification.new do |s| s.name = %q{mysql2} - s.version = "0.2.1" + 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-16} + s.date = %q{2010-08-19} s.email = %q{seniorlopez@gmail.com} s.extensions = ["ext/mysql2/extconf.rb"] s.extra_rdoc_files = [ From 864cf0f29140679cfdb04d1f3ed755e608a6fe99 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 20 Aug 2010 09:36:15 -0700 Subject: [PATCH 14/15] exposing client flags --- Rakefile | 2 ++ ext/mysql2/client.c | 8 ++++---- lib/mysql2/client.rb | 3 ++- spec/spec_helper.rb | 5 ++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Rakefile b/Rakefile index 7a1c103..df610fe 100644 --- a/Rakefile +++ b/Rakefile @@ -32,6 +32,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 task :default => :spec diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index f4a43f9..d779205 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -150,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) @@ -161,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 @@ -523,7 +523,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"); @@ -538,4 +538,4 @@ 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 +} diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index 81d08bc..3cd3aa8 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 = 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/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 From ce778998484641b5b14b7dc16828ea0f93dac9a6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 20 Aug 2010 09:56:36 -0700 Subject: [PATCH 15/15] connection flags can be passed to the constructor --- ext/mysql2/client.c | 105 +++++++++++++++++++++++++++++++++++++ lib/mysql2/client.rb | 2 +- spec/mysql2/client_spec.rb | 24 +++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index d779205..2a4d643 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -538,4 +538,109 @@ void init_mysql2_client() { intern_error_number_eql = rb_intern("error_number="); intern_sql_state_eql = rb_intern("sql_state="); +#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/mysql2/client.rb b/lib/mysql2/client.rb index 3cd3aa8..cbb6a60 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -30,7 +30,7 @@ module Mysql2 port = opts[:port] || 3306 database = opts[:database] socket = opts[:socket] - flags = 0 + flags = opts[:flags] || 0 connect user, pass, host, port, database, socket, flags end diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 2df7a77..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