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

4 5 6
module MysqlCompat
  # add all_hashes method to standard mysql-c bindings or pure ruby version
  def self.define_all_hashes_method!
7 8
    raise 'Mysql not loaded' unless defined?(::Mysql)

9 10
    target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
    return if target.instance_methods.include?('all_hashes')
11 12 13

    # Ruby driver has a version string and returns null values in each_hash
    # C driver >= 2.7 returns null values in each_hash
14 15 16 17 18 19
    if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
      target.class_eval <<-'end_eval'
      def all_hashes
        rows = []
        each_hash { |row| rows << row }
        rows
20
      end
21
      end_eval
22 23 24 25

    # adapters before 2.7 don't have a version constant
    # and don't return null values in each_hash
    else
26
      target.class_eval <<-'end_eval'
27 28 29 30 31 32 33 34
      def all_hashes
        rows = []
        all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
        each_hash { |row| rows << all_fields.dup.update(row) }
        rows
      end
      end_eval
    end
35 36 37 38

    unless target.instance_methods.include?('all_hashes')
      raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
    end
39 40 41
  end
end

D
Initial  
David Heinemeier Hansson 已提交
42 43
module ActiveRecord
  class Base
44 45
    def self.require_mysql
      # Include the MySQL driver if one hasn't already been loaded
J
Jeremy Kemper 已提交
46
      unless defined? Mysql
D
Initial  
David Heinemeier Hansson 已提交
47 48 49
        begin
          require_library_or_gem 'mysql'
        rescue LoadError => cannot_require_mysql
50
          # Use the bundled Ruby/MySQL driver if no driver is already in place
51
          begin
D
Initial  
David Heinemeier Hansson 已提交
52 53 54 55 56 57
            require 'active_record/vendor/mysql'
          rescue LoadError
            raise cannot_require_mysql
          end
        end
      end
J
Jeremy Kemper 已提交
58

59 60
      # Define Mysql::Result.all_hashes
      MysqlCompat.define_all_hashes_method!
61
    end
62

63 64
    # Establishes a connection to the database that's used by all Active Record objects.
    def self.mysql_connection(config) # :nodoc:
65
      config = config.symbolize_keys
D
Initial  
David Heinemeier Hansson 已提交
66 67 68 69 70
      host     = config[:host]
      port     = config[:port]
      socket   = config[:socket]
      username = config[:username] ? config[:username].to_s : 'root'
      password = config[:password].to_s
71

D
Initial  
David Heinemeier Hansson 已提交
72 73 74 75 76
      if config.has_key?(:database)
        database = config[:database]
      else
        raise ArgumentError, "No database specified. Missing argument: database."
      end
77

78
      require_mysql
79 80
      mysql = Mysql.init
      mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
81

82
      ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
D
Initial  
David Heinemeier Hansson 已提交
83 84
    end
  end
85

D
Initial  
David Heinemeier Hansson 已提交
86
  module ConnectionAdapters
87
    class MysqlColumn < Column #:nodoc:
88 89 90
      TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])

      def initialize(name, default, sql_type = nil, null = true)
91
        @original_default = default
92
        super
93
        @default = nil if missing_default_forged_as_empty_string?
94 95
      end

96 97
      private
        def simplified_type(field_type)
98
          return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
99
          return :string  if field_type =~ /enum/i
100 101
          super
        end
102 103 104 105 106 107 108 109 110

        # MySQL misreports NOT NULL column default when none is given.
        # We can't detect this for columns which may have a legitimate ''
        # default (string, text, binary) 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?
111
          !null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
112
        end
113 114
    end

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    # 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
130 131 132 133 134 135 136
    #
    # 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
137
    class MysqlAdapter < AbstractAdapter
138 139 140
      @@emulate_booleans = true
      cattr_accessor :emulate_booleans

141
      LOST_CONNECTION_ERROR_MESSAGES = [
142
        "Server shutdown in progress",
143 144
        "Broken pipe",
        "Lost connection to MySQL server during query",
145 146
        "MySQL server has gone away"
      ]
147

J
Jeremy Kemper 已提交
148
      def initialize(connection, logger, connection_options, config)
149
        super(connection, logger)
J
Jeremy Kemper 已提交
150
        @connection_options, @config = connection_options, config
151

152
        connect
153 154 155 156 157 158 159
      end

      def adapter_name #:nodoc:
        'MySQL'
      end

      def supports_migrations? #:nodoc:
160 161
        true
      end
162

163
      def native_database_types #:nodoc
164
        {
165
          :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
166 167 168 169
          :string      => { :name => "varchar", :limit => 255 },
          :text        => { :name => "text" },
          :integer     => { :name => "int", :limit => 11 },
          :float       => { :name => "float" },
170
          :decimal     => { :name => "decimal" },
171 172
          :datetime    => { :name => "datetime" },
          :timestamp   => { :name => "datetime" },
173
          :time        => { :name => "time" },
174 175 176
          :date        => { :name => "date" },
          :binary      => { :name => "blob" },
          :boolean     => { :name => "tinyint", :limit => 1 }
177 178 179
        }
      end

180

181 182
      # QUOTING ==================================================

183
      def quote(value, column = nil)
184
        if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
185 186
          s = column.class.string_to_binary(value).unpack("H*")[0]
          "x'#{s}'"
187 188
        elsif value.kind_of?(BigDecimal)
          "'#{value.to_s("F")}'"
189 190 191 192 193
        else
          super
        end
      end

194 195
      def quote_column_name(name) #:nodoc:
        "`#{name}`"
196 197
      end

198
      def quote_string(string) #:nodoc:
199
        @connection.quote(string)
D
Initial  
David Heinemeier Hansson 已提交
200
      end
201

202 203 204 205 206 207
      def quoted_true
        "1"
      end
      
      def quoted_false
        "0"
D
Initial  
David Heinemeier Hansson 已提交
208
      end
209

210

211 212 213
      # CONNECTION MANAGEMENT ====================================

      def active?
214 215 216 217 218
        if @connection.respond_to?(:stat)
          @connection.stat
        else
          @connection.query 'select 1'
        end
219 220 221 222 223 224 225

        # mysql-ruby doesn't raise an exception when stat fails.
        if @connection.respond_to?(:errno)
          @connection.errno.zero?
        else
          true
        end
226 227 228 229 230
      rescue Mysql::Error
        false
      end

      def reconnect!
231
        disconnect!
232
        connect
233
      end
234 235 236 237
      
      def disconnect!
        @connection.close rescue nil
      end
238 239


240
      # DATABASE STATEMENTS ======================================
241

242
      def execute(sql, name = nil) #:nodoc:
243
        log(sql, name) { @connection.query(sql) }
244
      rescue ActiveRecord::StatementInvalid => exception
245 246
        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."
247 248
        else
          raise
249
        end
D
Initial  
David Heinemeier Hansson 已提交
250
      end
251

252 253 254 255 256 257
      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:
258 259 260
        execute(sql, name)
        @connection.affected_rows
      end
261

262
      def begin_db_transaction #:nodoc:
263 264 265
        execute "BEGIN"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
266
      end
267

268
      def commit_db_transaction #:nodoc:
269 270 271
        execute "COMMIT"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
272
      end
273

274
      def rollback_db_transaction #:nodoc:
275 276 277
        execute "ROLLBACK"
      rescue Exception
        # Transactions aren't supported
D
Initial  
David Heinemeier Hansson 已提交
278
      end
279

280

281
      def add_limit_offset!(sql, options) #:nodoc
282 283 284
        if limit = options[:limit]
          unless offset = options[:offset]
            sql << " LIMIT #{limit}"
285
          else
286
            sql << " LIMIT #{offset}, #{limit}"
287
          end
288
        end
289
      end
290

291 292 293 294

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

      def structure_dump #:nodoc:
295 296 297 298 299 300 301 302
        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')
303 304 305 306 307
          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 已提交
308 309 310
        drop_database(name)
        create_database(name)
      end
311

312
      def create_database(name) #:nodoc:
313
        execute "CREATE DATABASE `#{name}`"
314 315 316
      end
      
      def drop_database(name) #:nodoc:
317
        execute "DROP DATABASE IF EXISTS `#{name}`"
D
Initial  
David Heinemeier Hansson 已提交
318
      end
319

320 321 322
      def current_database
        select_one("SELECT DATABASE() as db")["db"]
      end
323 324 325 326 327

      def tables(name = nil) #:nodoc:
        tables = []
        execute("SHOW TABLES", name).each { |field| tables << field[0] }
        tables
D
Initial  
David Heinemeier Hansson 已提交
328
      end
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354

      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
355 356 357 358
      
      def rename_table(name, new_name)
        execute "RENAME TABLE #{name} TO #{new_name}"
      end  
359 360

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

363
        execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
364
      end
365

366
      def change_column(table_name, column_name, type, options = {}) #:nodoc:
367 368 369
        if options[:default].nil?
          options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
        end
370
        
371
        change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
372 373 374 375
        add_column_options!(change_column_sql, options)
        execute(change_column_sql)
      end

376
      def rename_column(table_name, column_name, new_column_name) #:nodoc:
377 378 379 380
        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

381

D
Initial  
David Heinemeier Hansson 已提交
382
      private
383 384 385
        def connect
          encoding = @config[:encoding]
          if encoding
386
            @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
387
          end
388
          @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
389 390 391 392
          @connection.real_connect(*@connection_options)
          execute("SET NAMES '#{encoding}'") if encoding
        end

D
Initial  
David Heinemeier Hansson 已提交
393
        def select(sql, name = nil)
394 395
          @connection.query_with_result = true
          result = execute(sql, name)
396
          rows = result.all_hashes
397
          result.free
D
Initial  
David Heinemeier Hansson 已提交
398 399
          rows
        end
400

401 402 403
        def supports_views?
          version[0] >= 5
        end
404

405 406 407
        def version
          @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
        end
D
Initial  
David Heinemeier Hansson 已提交
408 409
    end
  end
410
end