diff options
5 files changed, 726 insertions, 1117 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb new file mode 100644 index 0000000000..72cf490d7e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -0,0 +1,552 @@ +require 'active_support/core_ext/object/blank' + +module ActiveRecord +  module ConnectionAdapters +    class AbstractMysqlAdapter < AbstractAdapter +      class Column < ConnectionAdapters::Column +        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 + +        private + +        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 + +      ## +      # :singleton-method: +      # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> +      # as boolean. If you wish to disable this emulation (which was the default +      # behavior in versions 0.13.1 and earlier) you can add the following line +      # to your application.rb file: +      # +      #   ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false +      class_attribute :emulate_booleans +      self.emulate_booleans = true + +      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 } +      } + +      # FIXME: Make the first parameter more similar for the two adapters +      def initialize(connection, logger, connection_options, config) +        super(connection, logger) +        @connection_options, @config = connection_options, config +        @quoted_column_names, @quoted_table_names = {}, {} +      end + +      def self.visitor_for(pool) # :nodoc: +        Arel::Visitors::MySQL.new(pool) +      end + +      def adapter_name #:nodoc: +        self.class::ADAPTER_NAME +      end + +      # Returns true, since this connection adapter supports migrations. +      def supports_migrations? +        true +      end + +      def supports_primary_key? +        true +      end + +      # Returns true, since this connection adapter supports savepoints. +      def supports_savepoints? +        true +      end + +      def native_database_types +        NATIVE_DATABASE_TYPES +      end + +      # HELPER METHODS =========================================== + +      # The two drivers have slightly different ways of yielding hashes of results, so +      # this method must be implemented to provide a uniform interface. +      def each_hash(result) # :nodoc: +        raise NotImplementedError +      end + +      # Overridden by the adapters to instantiate their specific Column type. +      def new_column(field, default, type, null) # :nodoc: +        Column.new(field, default, type, null) +      end + +      # Must return the Mysql error number from the exception, if the exception has an +      # error number. +      def error_number(exception) # :nodoc: +        raise NotImplementedError +      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.to_s.gsub('`', '``')}`" +      end + +      def quote_table_name(name) #:nodoc: +        @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') +      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 + +      # DATABASE STATEMENTS ====================================== + +      # Executes the SQL statement in the context of this connection. +      def execute(sql, name = nil) +        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 + +      # MysqlAdapter has to free a result after using it, so we use this method to write +      # stuff in a abstract way without concerning ourselves about whether it needs to be +      # explicitly freed or not. +      def execute_and_free(sql, name = nil) #:nodoc: +        yield execute(sql, name) +      end + +      def update_sql(sql, name = nil) #:nodoc: +        super +        @connection.affected_rows +      end + +      def begin_db_transaction +        execute "BEGIN" +      rescue Exception +        # Transactions aren't supported +      end + +      def commit_db_transaction #:nodoc: +        execute "COMMIT" +      rescue Exception +        # Transactions aren't supported +      end + +      def rollback_db_transaction #:nodoc: +        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 + +      # In the simple case, MySQL allows us to place JOINs directly into the UPDATE +      # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support +      # these, we must use a subquery. However, MySQL is too stupid to create a +      # temporary table for this automatically, so we have to give it some prompting +      # in the form of a subsubquery. Ugh! +      def join_to_update(update, select) #:nodoc: +        if select.limit || select.offset || select.orders.any? +          subsubselect = select.clone +          subsubselect.projections = [update.key] + +          subselect = Arel::SelectManager.new(select.engine) +          subselect.project Arel.sql(update.key.name) +          subselect.from subsubselect.as('__active_record_temp') + +          update.where update.key.in(subselect) +        else +          update.table select.source +          update.wheres = select.constraints +        end +      end + +      # SCHEMA STATEMENTS ======================================== + +      def structure_dump #:nodoc: +        if supports_views? +          sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" +        else +          sql = "SHOW TABLES" +        end + +        select_all(sql).map do |table| +          table.delete('Table_type') +          sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" +          exec_without_stmt(sql).first['Create Table'] + ";\n\n" +        end.join("") +      end + +      # Drops the database specified on the +name+ attribute +      # and creates it again using the provided +options+. +      def recreate_database(name, options = {}) +        drop_database(name) +        create_database(name, options) +      end + +      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. +      # 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 + +      # Drops a MySQL database. +      # +      # Example: +      #   drop_database('sebastian_development') +      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, database = nil) #:nodoc: +        sql = ["SHOW TABLES", database].compact.join(' IN ') + +        execute_and_free(sql, 'SCHEMA') do |result| +          result.collect { |field| field.first } +        end +      end + +      def table_exists?(name) +        return true if super + +        name          = name.to_s +        schema, table = name.split('.', 2) + +        unless table # A table was provided without a schema +          table  = schema +          schema = nil +        end + +        tables(nil, schema).include? table +      end + +      # Returns an array of indexes for the given table. +      def indexes(table_name, name = nil) #:nodoc: +        indexes = [] +        current_index = nil +        execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| +          each_hash(result) 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].to_i == 0, [], []) +            end + +            indexes.last.columns << row[:Column_name] +            indexes.last.lengths << row[:Sub_part] +          end +        end + +        indexes +      end + +      # Returns an array of +Column+ objects for the table specified by +table_name+. +      def columns(table_name, name = nil)#:nodoc: +        sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" +        execute_and_free(sql, 'SCHEMA') do |result| +          each_hash(result).map do |field| +            new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") +          end +        end +      end + +      def create_table(table_name, options = {}) #:nodoc: +        super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) +      end + +      # Renames a table. +      # +      # Example: +      #   rename_table('octopuses', 'octopi') +      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 = {}) +        execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") +      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 = {}) #:nodoc: +        execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") +      end + +      def rename_column(table_name, column_name, new_column_name) #:nodoc: +        execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") +      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 + +      # SHOW VARIABLES LIKE 'name' +      def show_variable(name) +        variables = select_all("SHOW VARIABLES LIKE '#{name}'") +        variables.first['Value'] unless variables.empty? +      end + +      # Returns a table's primary key and belonging sequence. +      def pk_and_sequence_for(table) +        execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result| +          keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] } +          keys.length == 1 ? [keys.first, nil] : nil +        end +      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_modifier(node) +        Arel::Nodes::Bin.new(node) +      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) + +        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) +        case error_number(exception) +        when 1062 +          RecordNotUnique.new(message, exception) +        when 1452 +          InvalidForeignKey.new(message, exception) +        else +          super +        end +      end + +      def add_column_sql(table_name, column_name, type, options = {}) +        add_column_sql = "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) +        add_column_sql +      end + +      def change_column_sql(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 = "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) +        change_column_sql +      end + +      def rename_column_sql(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 = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" +        add_column_options!(rename_column_sql, options) +        rename_column_sql +      end + +      private + +      def supports_views? +        version[0] >= 5 +      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/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index ef51f5ebca..00d9caa8ee 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +require 'active_record/connection_adapters/abstract_mysql_adapter'  gem 'mysql2', '~> 0.3.6'  require 'mysql2' @@ -20,31 +20,13 @@ module ActiveRecord    end    module ConnectionAdapters -    class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: -    end - -    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 +    class Mysql2Adapter < AbstractMysqlAdapter +      class Column < AbstractMysqlAdapter::Column # :nodoc: +        BOOL = "tinyint(1)" -      def has_default? -        return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns -        super -      end +        private -      private +        # FIXME: Combine with the mysql version and move to abstract adapter          def simplified_type(field_type)            return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) @@ -56,155 +38,45 @@ module ActiveRecord              super            end          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 +      end        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 = {}, {} +        super          configure_connection        end -      def self.visitor_for(pool) # :nodoc: -        Arel::Visitors::MySQL.new(pool) -      end - -      def adapter_name -        ADAPTER_NAME -      end - -      # Returns true, since this connection adapter supports migrations. -      def supports_migrations? -        true -      end - -      def supports_primary_key? -        true -      end - -      # Returns true, since this connection adapter supports savepoints. -      def supports_savepoints? -        true -      end - -      def native_database_types -        NATIVE_DATABASE_TYPES -      end +      # HELPER METHODS =========================================== -      # 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") +      def each_hash(result) # :nodoc: +        if block_given? +          result.each(:as => :hash, :symbolize_keys => true) do |row| +            yield row +          end          else -          super +          to_enum(:each_hash, result)          end        end -      def quote_column_name(name) #:nodoc: -        @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" +      def new_column(field, default, type, null) # :nodoc: +        Column.new(field, default, type, null)        end -      def quote_table_name(name) #:nodoc: -        @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') +      def error_number(exception) +        exception.error_number if exception.respond_to?(:error_number)        end +      # QUOTING ================================================== +        def quote_string(string)          @connection.escape(string)        end -      def quoted_true -        QUOTED_TRUE -      end - -      def quoted_false -        QUOTED_FALSE -      end -        def substitute_at(column, index)          Arel.sql "\0"        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? @@ -217,11 +89,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 -        # Disconnects from the database if already connected.        # Otherwise, this method does nothing.        def disconnect! @@ -277,17 +144,22 @@ module ActiveRecord          # 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 + +        super +      end + +      def exec_query(sql, name = 'SQL', binds = []) +        result = execute(sql, name) +        ActiveRecord::Result.new(result.fields, result.to_a) +      end + +      alias exec_without_stmt exec_query + +      # Returns an array of record hashes with the column names as keys and +      # column values as values. +      def select(sql, name = nil, binds = []) +        binds = binds.dup +        exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a        end        def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -316,379 +188,35 @@ module ActiveRecord          @connection.last_id        end -      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 - -      # 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 - -      # Drops the database specified on the +name+ attribute -      # and creates it again using the provided +options+. -      def recreate_database(name, options = {}) -        drop_database(name) -        create_database(name, options) -      end - -      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. -      # 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 - -      # Drops a MySQL database. -      # -      # Example: -      #   drop_database('sebastian_development') -      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, database = nil) #:nodoc: -        sql = ["SHOW TABLES", database].compact.join(' IN ') -        execute(sql, 'SCHEMA').collect do |field| -          field.first -        end -      end - -      def table_exists?(name) -        return true if super - -        name          = name.to_s -        schema, table = name.split('.', 2) - -        unless table # A table was provided without a schema -          table  = schema -          schema = nil -        end - -        tables(nil, schema).include? table -      end - -      # Returns an array of indexes for the given table. -      def indexes(table_name, name = nil) -        indexes = [] -        current_index = nil -        result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') -        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 << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) -          end - -          indexes.last.columns << row[:Column_name] -          indexes.last.lengths << row[:Sub_part] -        end -        indexes -      end - -      # Returns an array of +Mysql2Column+ objects for the table specified by +table_name+. -      def columns(table_name, name = nil) -        sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" -        columns = [] -        result = execute(sql, 'SCHEMA') -        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 - -      # Renames a table. -      # -      # Example: -      #   rename_table('octopuses', 'octopi') -      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 +      private -      def add_column_position!(sql, options) -        if options[:first] -          sql << " FIRST" -        elsif options[:after] -          sql << " AFTER #{quote_column_name(options[:after])}" -        end +      def connect +        @connection = Mysql2::Client.new(@config) +        configure_connection        end -      # SHOW VARIABLES LIKE 'name'. -      def show_variable(name) -        variables = select_all("SHOW VARIABLES LIKE '#{name}'") -        variables.first['Value'] unless variables.empty? -      end +      def configure_connection +        @connection.query_options.merge!(:as => :array) -      # Returns a table's primary key and belonging sequence. -      def pk_and_sequence_for(table) -        keys = [] -        result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') -        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 +        # 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] -      # 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 +        # make sure we set the encoding +        variable_assignments << "NAMES '#{encoding}'" if encoding -      def case_sensitive_modifier(node) -        Arel::Nodes::Bin.new(node) -      end +        # increase timeout so mysql server doesn't disconnect us +        wait_timeout = @config[:wait_timeout] +        wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) +        variable_assignments << "@@wait_timeout = #{wait_timeout}" -      def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) -        where_sql +        execute("SET #{variable_assignments.join(', ')}", :skip_logging)        end -      # In the simple case, MySQL allows us to place JOINs directly into the UPDATE -      # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support -      # these, we must use a subquery. However, MySQL is too stupid to create a -      # temporary table for this automatically, so we have to give it some prompting -      # in the form of a subsubquery. Ugh! -      def join_to_update(update, select) #:nodoc: -        if select.limit || select.offset || select.orders.any? -          subsubselect = select.clone -          subsubselect.projections = [update.key] - -          subselect = Arel::SelectManager.new(select.engine) -          subselect.project Arel.sql(update.key.name) -          subselect.from subsubselect.as('__active_record_temp') - -          update.where update.key.in(subselect) -        else -          update.table select.source -          update.wheres = select.constraints -        end +      def version +        @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }        end - -      protected -        def quoted_columns_for_index(column_names, options = {}) -          length = options[:length] if options.is_a?(Hash) - -          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] - -          # make sure we set the encoding -          variable_assignments << "NAMES '#{encoding}'" if encoding - -          # increase timeout so mysql server doesn't disconnect us -          wait_timeout = @config[:wait_timeout] -          wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) -          variable_assignments << "@@wait_timeout = #{wait_timeout}" - -          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, binds = []) -          binds = binds.dup -          exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a -        end - -        def exec_query(sql, name = 'SQL', binds = []) -          @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - -          log(sql, name, binds) do -            begin -              result = @connection.query(sql) -            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 - -            ActiveRecord::Result.new(result.fields, result.to_a) -          end -        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/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index b844e5ab10..d61875195a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -1,6 +1,5 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' -require 'set' +require 'active_record/connection_adapters/abstract_mysql_adapter' +require 'active_support/core_ext/hash/keys'  gem 'mysql', '~> 2.8.1'  require 'mysql' @@ -40,9 +39,28 @@ module ActiveRecord    end    module ConnectionAdapters -    class MysqlColumn < Column #:nodoc: -      class << self -        def string_to_time(value) +    # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with +    # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). +    # +    # Options: +    # +    # * <tt>:host</tt> - Defaults to "localhost". +    # * <tt>:port</tt> - Defaults to 3306. +    # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". +    # * <tt>:username</tt> - Defaults to "root" +    # * <tt>:password</tt> - Defaults to nothing. +    # * <tt>:database</tt> - The name of the database. No default, must be provided. +    # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. +    # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). +    # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. +    # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. +    # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. +    # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. +    # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. +    # +    class MysqlAdapter < AbstractMysqlAdapter +      class Column < AbstractMysqlAdapter::Column #:nodoc: +        def self.string_to_time(value)            return super unless Mysql::Time === value            new_time(              value.year, @@ -54,152 +72,36 @@ module ActiveRecord              value.second_part)          end -        def string_to_dummy_time(v) +        def self.string_to_dummy_time(v)            return super unless Mysql::Time === v            new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)          end -        def string_to_date(v) +        def self.string_to_date(v)            return super unless Mysql::Time === v            new_date(v.year, v.month, v.day)          end -      end -      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 +        private -      def has_default? -        return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns -        super -      end - -      private +        # FIXME: Combine with the mysql2 version and move to abstract adapter          def simplified_type(field_type)            return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")            return :string  if field_type =~ /enum/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 - -    # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with -    # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). -    # -    # Options: -    # -    # * <tt>:host</tt> - Defaults to "localhost". -    # * <tt>:port</tt> - Defaults to 3306. -    # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". -    # * <tt>:username</tt> - Defaults to "root" -    # * <tt>:password</tt> - Defaults to nothing. -    # * <tt>:database</tt> - The name of the database. No default, must be provided. -    # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. -    # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). -    # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. -    # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. -    # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. -    # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. -    # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. -    # -    class MysqlAdapter < AbstractAdapter - -      ## -      # :singleton-method: -      # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> -      # as boolean. If you wish to disable this emulation (which was the default -      # behavior in versions 0.13.1 and earlier) you can add the following line -      # to your application.rb file: -      # -      #   ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false -      cattr_accessor :emulate_booleans -      self.emulate_booleans = true +      end        ADAPTER_NAME = 'MySQL' -      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 = {}, {} +        super          @statements = {}          @client_encoding = nil          connect        end -      def self.visitor_for(pool) # :nodoc: -        Arel::Visitors::MySQL.new(pool) -      end - -      def adapter_name #:nodoc: -        ADAPTER_NAME -      end - +      # FIXME: Move to abstract adapter        def supports_bulk_alter? #:nodoc:          true        end @@ -210,78 +112,39 @@ module ActiveRecord          true        end -      # Returns true, since this connection adapter supports migrations. -      def supports_migrations? #:nodoc: -        true -      end +      # HELPER METHODS =========================================== -      # Returns true. -      def supports_primary_key? #:nodoc: -        true +      def each_hash(result) # :nodoc: +        if block_given? +          result.each_hash do |row| +            row.symbolize_keys! +            yield row +          end +        else +          to_enum(:each_hash, result) +        end        end -      # Returns true, since this connection adapter supports savepoints. -      def supports_savepoints? #:nodoc: -        true +      def new_column(field, default, type, null) # :nodoc: +        Column.new(field, default, type, null)        end -      def native_database_types #:nodoc: -        NATIVE_DATABASE_TYPES +      def error_number(exception) # :nodoc: +        exception.errno if exception.respond_to?(:errno)        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 type_cast(value, column)          return super unless value == true || value == false          value ? 1 : 0        end -      def quote_column_name(name) #:nodoc: -        @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" -      end - -      def quote_table_name(name) #:nodoc: -        @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') -      end -        def quote_string(string) #:nodoc:          @connection.quote(string)        end -      def quoted_true -        QUOTED_TRUE -      end - -      def quoted_false -        QUOTED_FALSE -      end - -      # REFERENTIAL INTEGRITY ==================================== - -      def disable_referential_integrity #: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? @@ -425,20 +288,11 @@ module ActiveRecord          end        end -      # Executes an SQL query and returns a MySQL::Result object. Note that you have to free -      # the Result object after you're done using it. -      def execute(sql, name = nil) #:nodoc: -        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 +      def execute_and_free(sql, name = nil) +        result = execute(sql, name) +        ret = yield result +        result.free +        ret        end        def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: @@ -447,11 +301,6 @@ module ActiveRecord        end        alias :create :insert_sql -      def update_sql(sql, name = nil) #:nodoc: -        super -        @connection.affected_rows -      end -        def exec_delete(sql, name, binds)          log(sql, name, binds) do            exec_stmt(sql, name, binds) do |cols, stmt| @@ -467,172 +316,8 @@ module ActiveRecord          # Transactions aren't supported        end -      def commit_db_transaction #:nodoc: -        execute "COMMIT" -      rescue Exception -        # Transactions aren't supported -      end - -      def rollback_db_transaction #:nodoc: -        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 - -      # In the simple case, MySQL allows us to place JOINs directly into the UPDATE -      # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support -      # these, we must use a subquery. However, MySQL is too stupid to create a -      # temporary table for this automatically, so we have to give it some prompting -      # in the form of a subsubquery. Ugh! -      def join_to_update(update, select) #:nodoc: -        if select.limit || select.offset || select.orders.any? -          subsubselect = select.clone -          subsubselect.projections = [update.key] - -          subselect = Arel::SelectManager.new(select.engine) -          subselect.project Arel.sql(update.key.name) -          subselect.from subsubselect.as('__active_record_temp') - -          update.where update.key.in(subselect) -        else -          update.table select.source -          update.wheres = select.constraints -        end -      end -        # SCHEMA STATEMENTS ======================================== -      def structure_dump #:nodoc: -        if supports_views? -          sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" -        else -          sql = "SHOW TABLES" -        end - -        select_all(sql).map do |table| -          table.delete('Table_type') -          sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" -          exec_without_stmt(sql).first['Create Table'] + ";\n\n" -        end.join("") -      end - -      # Drops the database specified on the +name+ attribute -      # and creates it again using the provided +options+. -      def recreate_database(name, options = {}) #:nodoc: -        drop_database(name) -        create_database(name, options) -      end - -      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. -      # 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 - -      # Drops a MySQL database. -      # -      # Example: -      #   drop_database 'sebastian_development' -      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, database = nil) #:nodoc: -        result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA') -        tables = result.collect { |field| field[0] } -        result.free -        tables -      end - -      def table_exists?(name) -        return true if super - -        name          = name.to_s -        schema, table = name.split('.', 2) - -        unless table # A table was provided without a schema -          table  = schema -          schema = nil -        end - -        tables(nil, schema).include? table -      end - -      # Returns an array of indexes for the given table. -      def indexes(table_name, name = nil)#:nodoc: -        indexes = [] -        current_index = nil -        result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) -        result.each do |row| -          if current_index != row[2] -            next if row[2] == "PRIMARY" # skip the primary key -            current_index = row[2] -            indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], []) -          end - -          indexes.last.columns << row[4] -          indexes.last.lengths << row[7] -        end -        result.free -        indexes -      end - -      # Returns an array of +MysqlColumn+ objects for the table specified by +table_name+. -      def columns(table_name, name = nil)#:nodoc: -        sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" -        result = execute(sql, 'SCHEMA') -        columns = result.collect { |field| MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } -        result.free -        columns -      end - -      def create_table(table_name, options = {}) #:nodoc: -        super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) -      end - -      # Renames a table. -      # -      # Example: -      #   rename_table('octopuses', 'octopi') -      def rename_table(table_name, new_name) -        execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" -      end -        def bulk_change_table(table_name, operations) #:nodoc:          sqls = operations.map do |command, args|            table, arguments = args.shift, args @@ -648,177 +333,33 @@ module ActiveRecord          execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")        end -      def add_column(table_name, column_name, type, options = {}) -        execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") -      end - -      def change_column_default(table_name, column_name, default) #:nodoc: -        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 = {}) #:nodoc: -        execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") -      end - -      def rename_column(table_name, column_name, new_column_name) #:nodoc: -        execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") -      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 +      protected -      # SHOW VARIABLES LIKE 'name' -      def show_variable(name) -        variables = select_all("SHOW VARIABLES LIKE '#{name}'") -        variables.first['Value'] unless variables.empty? +      def remove_column_sql(table_name, *column_names) +        columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }        end +      alias :remove_columns_sql :remove_column -      # Returns a table's primary key and belonging sequence. -      def pk_and_sequence_for(table) #:nodoc: -        keys = [] -        result = execute("describe #{quote_table_name(table)}", 'SCHEMA') -        result.each_hash do |h| -          keys << h["Field"]if h["Key"] == "PRI" -        end -        result.free -        keys.length == 1 ? [keys.first, nil] : nil +      def add_index_sql(table_name, column_name, options = {}) +        index_name, index_type, index_columns = add_index_options(table_name, column_name, options) +        "ADD #{index_type} INDEX #{index_name} (#{index_columns})"        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 +      def remove_index_sql(table_name, options = {}) +        index_name = index_name_for_remove(table_name, options) +        "DROP INDEX #{index_name}"        end -      def case_sensitive_modifier(node) -        Arel::Nodes::Bin.new(node) +      def add_timestamps_sql(table_name) +        [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]        end -      def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) -        where_sql +      def remove_timestamps_sql(table_name) +        [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]        end -      protected -        def quoted_columns_for_index(column_names, options = {}) -          length = options[:length] if options.is_a?(Hash) - -          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?(:errno) - -          case exception.errno -          when 1062 -            RecordNotUnique.new(message, exception) -          when 1452 -            InvalidForeignKey.new(message, exception) -          else -            super -          end -        end - -        def add_column_sql(table_name, column_name, type, options = {}) -          add_column_sql = "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) -          add_column_sql -        end - -        def remove_column_sql(table_name, *column_names) -          columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } -        end -        alias :remove_columns_sql :remove_column - -        def change_column_sql(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 = "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) -          change_column_sql -        end - -        def rename_column_sql(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 = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" -          add_column_options!(rename_column_sql, options) -          rename_column_sql -        end - -        def add_index_sql(table_name, column_name, options = {}) -          index_name, index_type, index_columns = add_index_options(table_name, column_name, options) -          "ADD #{index_type} INDEX #{index_name} (#{index_columns})" -        end - -        def remove_index_sql(table_name, options = {}) -          index_name = index_name_for_remove(table_name, options) -          "DROP INDEX #{index_name}" -        end - -        def add_timestamps_sql(table_name) -          [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] -        end - -        def remove_timestamps_sql(table_name) -          [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] -        end -        private +        def exec_stmt(sql, name, binds)          cache = {}          if binds.empty? @@ -830,7 +371,6 @@ module ActiveRecord            stmt = cache[:stmt]          end -          begin            stmt.execute(*binds.map { |col, val| type_cast(val, col) })          rescue Mysql::Error => e @@ -859,59 +399,48 @@ module ActiveRecord          result        end -        def connect -          encoding = @config[:encoding] -          if encoding -            @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil -          end - -          if @config[:sslca] || @config[:sslkey] -            @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) -          end +      def connect +        encoding = @config[:encoding] +        if encoding +          @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil +        end -          @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] -          @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] -          @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] +        if @config[:sslca] || @config[:sslkey] +          @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) +        end -          @connection.real_connect(*@connection_options) +        @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] +        @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] +        @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] -          # reconnect must be set after real_connect is called, because real_connect sets it to false internally -          @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) +        @connection.real_connect(*@connection_options) -          configure_connection -        end +        # reconnect must be set after real_connect is called, because real_connect sets it to false internally +        @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) -        def configure_connection -          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 +        configure_connection +      end -        def select(sql, name = nil, binds = []) -          @connection.query_with_result = true -          rows = exec_query(sql, name, binds).to_a -          @connection.more_results && @connection.next_result    # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped -          rows -        end +      def configure_connection +        encoding = @config[:encoding] +        execute("SET NAMES '#{encoding}'", :skip_logging) if encoding -        def supports_views? -          version[0] >= 5 -        end +        # 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 the version of the connected MySQL server. -        def version -          @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } -        end +      def select(sql, name = nil, binds = []) +        @connection.query_with_result = true +        rows = exec_query(sql, name, binds).to_a +        @connection.more_results && @connection.next_result    # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped +        rows +      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 +      # Returns the version of the connected MySQL server. +      def version +        @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } +      end      end    end  end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 509baacaef..94fc3564df 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -2,7 +2,7 @@ require "cases/helper"  class ActiveSchemaTest < ActiveRecord::TestCase    def setup -    ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do +    ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do        alias_method :execute_without_stub, :execute        remove_method :execute        def execute(sql, name = nil) return sql end @@ -10,7 +10,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase    end    def teardown -    ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do +    ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do        remove_method :execute        alias_method :execute, :execute_without_stub      end @@ -99,7 +99,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase    private      def with_real_execute        #we need to actually modify some data, so we make execute point to the original method -      ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do +      ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do          alias_method :execute_with_stub, :execute          remove_method :execute          alias_method :execute, :execute_without_stub @@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase        yield      ensure        #before finishing, we restore the alias to the mock-up method -      ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do +      ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do          remove_method :execute          alias_method :execute, :execute_with_stub        end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index d1dddd4c2c..14884e42af 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -58,68 +58,68 @@ module ActiveRecord        if current_adapter?(:MysqlAdapter)          def test_should_set_default_for_mysql_binary_data_types -          binary_column = MysqlColumn.new("title", "a", "binary(1)") +          binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)")            assert_equal "a", binary_column.default -          varbinary_column = MysqlColumn.new("title", "a", "varbinary(1)") +          varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)")            assert_equal "a", varbinary_column.default          end          def test_should_not_set_default_for_blob_and_text_data_types            assert_raise ArgumentError do -            MysqlColumn.new("title", "a", "blob") +            MysqlAdapter::Column.new("title", "a", "blob")            end            assert_raise ArgumentError do -            MysqlColumn.new("title", "Hello", "text") +            MysqlAdapter::Column.new("title", "Hello", "text")            end -          text_column = MysqlColumn.new("title", nil, "text") +          text_column = MysqlAdapter::Column.new("title", nil, "text")            assert_equal nil, text_column.default -          not_null_text_column = MysqlColumn.new("title", nil, "text", false) +          not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false)            assert_equal "", not_null_text_column.default          end          def test_has_default_should_return_false_for_blog_and_test_data_types -          blob_column = MysqlColumn.new("title", nil, "blob") +          blob_column = MysqlAdapter::Column.new("title", nil, "blob")            assert !blob_column.has_default? -          text_column = MysqlColumn.new("title", nil, "text") +          text_column = MysqlAdapter::Column.new("title", nil, "text")            assert !text_column.has_default?          end        end        if current_adapter?(:Mysql2Adapter)          def test_should_set_default_for_mysql_binary_data_types -          binary_column = Mysql2Column.new("title", "a", "binary(1)") +          binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)")            assert_equal "a", binary_column.default -          varbinary_column = Mysql2Column.new("title", "a", "varbinary(1)") +          varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)")            assert_equal "a", varbinary_column.default          end          def test_should_not_set_default_for_blob_and_text_data_types            assert_raise ArgumentError do -            Mysql2Column.new("title", "a", "blob") +            Mysql2Adapter::Column.new("title", "a", "blob")            end            assert_raise ArgumentError do -            Mysql2Column.new("title", "Hello", "text") +            Mysql2Adapter::Column.new("title", "Hello", "text")            end -          text_column = Mysql2Column.new("title", nil, "text") +          text_column = Mysql2Adapter::Column.new("title", nil, "text")            assert_equal nil, text_column.default -          not_null_text_column = Mysql2Column.new("title", nil, "text", false) +          not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false)            assert_equal "", not_null_text_column.default          end          def test_has_default_should_return_false_for_blog_and_test_data_types -          blob_column = Mysql2Column.new("title", nil, "blob") +          blob_column = Mysql2Adapter::Column.new("title", nil, "blob")            assert !blob_column.has_default? -          text_column = Mysql2Column.new("title", nil, "text") +          text_column = Mysql2Adapter::Column.new("title", nil, "text")            assert !text_column.has_default?          end        end  | 
