mysql_adapter.rb 9.8 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1
require 'active_record/connection_adapters/abstract_adapter'
2

D
Initial  
David Heinemeier Hansson 已提交
3 4
module ActiveRecord
  class Base
5
    # Establishes a connection to the database that's used by all Active Record objects.
D
Initial  
David Heinemeier Hansson 已提交
6 7 8 9 10 11 12
    def self.mysql_connection(config) # :nodoc:
      unless self.class.const_defined?(:Mysql)
        begin
          # Only include the MySQL driver if one hasn't already been loaded
          require_library_or_gem 'mysql'
        rescue LoadError => cannot_require_mysql
          # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
13
          begin
D
Initial  
David Heinemeier Hansson 已提交
14
            require 'active_record/vendor/mysql'
15
            require 'active_record/vendor/mysql411'
D
Initial  
David Heinemeier Hansson 已提交
16 17 18 19 20
          rescue LoadError
            raise cannot_require_mysql
          end
        end
      end
21

22
      config = config.symbolize_keys
D
Initial  
David Heinemeier Hansson 已提交
23 24 25 26 27
      host     = config[:host]
      port     = config[:port]
      socket   = config[:socket]
      username = config[:username] ? config[:username].to_s : 'root'
      password = config[:password].to_s
28

D
Initial  
David Heinemeier Hansson 已提交
29 30 31 32 33
      if config.has_key?(:database)
        database = config[:database]
      else
        raise ArgumentError, "No database specified. Missing argument: database."
      end
34

35 36
      mysql = Mysql.init
      mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
37
      ConnectionAdapters::MysqlAdapter.new(mysql.real_connect(host, username, password, database, port, socket), logger, [host, username, password, database, port, socket])
D
Initial  
David Heinemeier Hansson 已提交
38 39
    end
  end
40

D
Initial  
David Heinemeier Hansson 已提交
41
  module ConnectionAdapters
42 43 44 45 46 47 48 49
    class MysqlColumn < Column #:nodoc:
      private
        def simplified_type(field_type)
          return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase == "tinyint(1)"
          super
        end
    end

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
    # 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>: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
65 66 67 68 69 70 71
    #
    # By default, the MysqlAdapter will consider all columns of type tinyint(1)
    # 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 environment.rb file:
    #
    #   ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
72
    class MysqlAdapter < AbstractAdapter
73 74 75
      @@emulate_booleans = true
      cattr_accessor :emulate_booleans

76
      LOST_CONNECTION_ERROR_MESSAGES = [
77
        "Server shutdown in progress",
78 79
        "Broken pipe",
        "Lost connection to MySQL server during query",
80 81
        "MySQL server has gone away"
      ]
82 83 84 85 86 87 88 89 90 91 92

      def initialize(connection, logger, connection_options=nil)
        super(connection, logger)
        @connection_options = connection_options
      end

      def adapter_name #:nodoc:
        'MySQL'
      end

      def supports_migrations? #:nodoc:
93 94
        true
      end
95

96
      def native_database_types #:nodoc
97
        {
98
          :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
99 100 101 102 103 104 105 106 107 108
          :string      => { :name => "varchar", :limit => 255 },
          :text        => { :name => "text" },
          :integer     => { :name => "int", :limit => 11 },
          :float       => { :name => "float" },
          :datetime    => { :name => "datetime" },
          :timestamp   => { :name => "datetime" },
          :time        => { :name => "datetime" },
          :date        => { :name => "date" },
          :binary      => { :name => "blob" },
          :boolean     => { :name => "tinyint", :limit => 1 }
109 110 111
        }
      end

112

113 114 115 116
      # QUOTING ==================================================

      def quote_column_name(name) #:nodoc:
        "`#{name}`"
117 118
      end

119 120
      def quote_string(string) #:nodoc:
        Mysql::quote(string)
D
Initial  
David Heinemeier Hansson 已提交
121
      end
122

123 124 125 126 127 128
      def quoted_true
        "1"
      end
      
      def quoted_false
        "0"
D
Initial  
David Heinemeier Hansson 已提交
129
      end
130

131

132
      # DATABASE STATEMENTS ======================================
133

134 135
      def select_all(sql, name = nil) #:nodoc:
        select(sql, name)
D
Initial  
David Heinemeier Hansson 已提交
136
      end
137

138 139 140
      def select_one(sql, name = nil) #:nodoc:
        result = select(sql, name)
        result.nil? ? nil : result.first
D
Initial  
David Heinemeier Hansson 已提交
141
      end
142

143
      def execute(sql, name = nil, retries = 2) #:nodoc:
144 145 146 147 148 149 150 151 152 153
        unless @logger
          @connection.query(sql)
        else
          log(sql, name) { @connection.query(sql) }
        end
      rescue ActiveRecord::StatementInvalid => exception
        if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message.split(":").first =~ /^#{msg}/ }
          @connection.real_connect(*@connection_options)
          unless @logger
            @connection.query(sql)
154
          else
155
            @logger.info "Retrying invalid statement with reopened connection"
156
            log(sql, name) { @connection.query(sql) }
157
          end
158 159
        elsif 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 update mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information."
160 161
        else
          raise
162
        end
D
Initial  
David Heinemeier Hansson 已提交
163
      end
164

165 166 167 168 169 170
      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
        execute(sql, name = nil)
        id_value || @connection.insert_id
      end

      def update(sql, name = nil) #:nodoc:
171 172 173
        execute(sql, name)
        @connection.affected_rows
      end
174

175
      alias_method :delete, :update #:nodoc:
176 177


178
      def begin_db_transaction #:nodoc:
179 180 181
        execute "BEGIN"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
182
      end
183

184
      def commit_db_transaction #:nodoc:
185 186 187
        execute "COMMIT"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
188
      end
189

190
      def rollback_db_transaction #:nodoc:
191 192 193
        execute "ROLLBACK"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
194
      end
195

196

197
      def add_limit_offset!(sql, options) #:nodoc
198 199
        if options[:limit]
          if options[:offset].blank?
200
            sql << " LIMIT #{options[:limit]}"
201 202
          else
            sql << " LIMIT #{options[:offset]}, #{options[:limit]}"
203
          end
204
        end
205
      end
206

207 208 209 210 211 212 213 214 215 216

      # SCHEMA STATEMENTS ========================================

      def structure_dump #:nodoc:
        select_all("SHOW TABLES").inject("") do |structure, table|
          structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
        end
      end

      def recreate_database(name) #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
217 218 219
        drop_database(name)
        create_database(name)
      end
220

221 222 223 224 225
      def create_database(name) #:nodoc:
        execute "CREATE DATABASE #{name}"
      end
      
      def drop_database(name) #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
226 227
        execute "DROP DATABASE IF EXISTS #{name}"
      end
228

229 230 231 232 233

      def tables(name = nil) #:nodoc:
        tables = []
        execute("SHOW TABLES", name).each { |field| tables << field[0] }
        tables
D
Initial  
David Heinemeier Hansson 已提交
234
      end
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262

      def indexes(table_name, name = nil)#:nodoc:
        indexes = []
        current_index = nil
        execute("SHOW KEYS FROM #{table_name}", name).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]
        end
        indexes
      end

      def columns(table_name, name = nil)#:nodoc:
        sql = "SHOW FIELDS FROM #{table_name}"
        columns = []
        execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
        columns
      end

      def create_table(name, options = {}) #:nodoc:
        super(name, {:options => "ENGINE=InnoDB"}.merge(options))
      end

      def change_column_default(table_name, column_name, default) #:nodoc:
263 264 265 266
        current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]

        change_column(table_name, column_name, current_type, { :default => default })
      end
267

268
      def change_column(table_name, column_name, type, options = {}) #:nodoc:
269 270 271
        options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
        
        change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
272 273 274 275
        add_column_options!(change_column_sql, options)
        execute(change_column_sql)
      end

276
      def rename_column(table_name, column_name, new_column_name) #:nodoc:
277 278 279 280
        current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
        execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
      end

281

D
Initial  
David Heinemeier Hansson 已提交
282 283
      private
        def select(sql, name = nil)
284 285
          @connection.query_with_result = true
          result = execute(sql, name)
D
Initial  
David Heinemeier Hansson 已提交
286
          rows = []
287
          all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
288
          result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
289
          result.free
D
Initial  
David Heinemeier Hansson 已提交
290 291 292 293
          rows
        end
    end
  end
294
end