abstract_mysql_adapter.rb 29.8 KB
Newer Older
1
require 'arel/visitors/bind_visitor'
2
require 'active_support/core_ext/string/strip'
3 4 5 6

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
7 8
      include Savepoints

9
      class SchemaCreation < AbstractAdapter::SchemaCreation
R
Rafael Mendonça França 已提交
10 11 12 13
        def visit_AddColumn(o)
          add_column_position!(super, column_options(o))
        end

14 15
        private

16 17 18 19
        def visit_DropForeignKey(name)
          "DROP FOREIGN KEY #{name}"
        end

20
        def visit_TableDefinition(o)
R
Rafael Mendonça França 已提交
21 22 23 24 25 26
          name = o.name
          create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "

          statements = o.columns.map { |c| accept c }
          statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })

27
          create_sql << "(#{statements.join(', ')}) " if statements.present?
28 29 30 31
          create_sql << "#{o.options}"
          create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
          create_sql
        end
32

33 34 35 36 37
        def visit_ChangeColumnDefinition(o)
          column = o.column
          options = o.options
          sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
          change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
L
Luke Steensen 已提交
38
          add_column_options!(change_column_sql, options.merge(column: column))
39
          add_column_position!(change_column_sql, options)
40 41
        end

42 43
        def add_column_position!(sql, options)
          if options[:first]
44
            sql << " FIRST"
45 46
          elsif options[:after]
            sql << " AFTER #{quote_column_name(options[:after])}"
47 48 49
          end
          sql
        end
50 51

        def index_in_create(table_name, column_name, options)
52
          index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
53
          "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
54
        end
55 56 57 58 59 60
      end

      def schema_creation
        SchemaCreation.new self
      end

61
      class Column < ConnectionAdapters::Column # :nodoc:
62
        attr_reader :collation, :strict, :extra
63

64
        def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
65
          @strict    = strict
66
          @collation = collation
67
          @extra     = extra
68
          super(name, default, cast_type, sql_type, null)
69
          assert_valid_default(default)
70
          extract_default
71 72
        end

73 74 75 76 77
        def extract_default
          if blob_or_text_column?
            @default = null || strict ? nil : ''
          elsif missing_default_forged_as_empty_string?(@default)
            @default = nil
78 79 80 81
          end
        end

        def has_default?
82
          return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
83 84
          super
        end
85

86 87 88
        def blob_or_text_column?
          sql_type =~ /blob/i || type == :text
        end
89

90 91 92 93
        def case_sensitive?
          collation && !collation.match(/_ci$/)
        end

94 95 96 97 98 99 100
        def ==(other)
          super &&
            collation == other.collation &&
            strict == other.strict &&
            extra == other.extra
        end

101 102 103 104 105 106 107 108 109 110 111 112
        private

        # 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
113 114 115 116 117 118

        def assert_valid_default(default)
          if blob_or_text_column? && default.present?
            raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
          end
        end
119 120 121 122

        def attributes_for_hash
          super + [collation, strict, extra]
        end
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
      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 = {
145
        :primary_key => "int(11) auto_increment PRIMARY KEY",
146 147 148 149 150 151 152 153 154 155 156 157
        :string      => { :name => "varchar", :limit => 255 },
        :text        => { :name => "text" },
        :integer     => { :name => "int", :limit => 4 },
        :float       => { :name => "float" },
        :decimal     => { :name => "decimal" },
        :datetime    => { :name => "datetime" },
        :time        => { :name => "time" },
        :date        => { :name => "date" },
        :binary      => { :name => "blob" },
        :boolean     => { :name => "tinyint", :limit => 1 }
      }

158 159 160
      INDEX_TYPES  = [:fulltext, :spatial]
      INDEX_USINGS = [:btree, :hash]

161 162 163 164 165
      # 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 = {}, {}
166

A
Aaron Patterson 已提交
167 168
        @visitor = Arel::Visitors::MySQL.new self

169
        if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
170
          @prepared_statements = true
171
        else
A
Aaron Patterson 已提交
172 173 174 175
          @prepared_statements = false
        end
      end

176 177 178 179 180 181 182 183 184
      # Returns true, since this connection adapter supports migrations.
      def supports_migrations?
        true
      end

      def supports_primary_key?
        true
      end

185 186 187 188
      def supports_bulk_alter? #:nodoc:
        true
      end

189
      # Technically MySQL allows to create indexes with the sort order syntax
190 191 192 193 194
      # but at the moment (5.5) it doesn't yet implement them
      def supports_index_sort_order?
        true
      end

195 196 197 198 199 200 201 202
      # MySQL 4 technically support transaction isolation, but it is affected by a bug
      # where the transaction level gets persisted for the whole session:
      #
      # http://bugs.mysql.com/bug.php?id=39170
      def supports_transaction_isolation?
        version[0] >= 5
      end

203 204 205 206
      def supports_indexes_in_create?
        true
      end

207 208 209 210
      def supports_foreign_keys?
        true
      end

211 212 213 214
      def supports_views?
        version[0] >= 5
      end

215 216 217 218
      def native_database_types
        NATIVE_DATABASE_TYPES
      end

219 220 221 222
      def index_algorithms
        { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
      end

223 224 225 226 227 228 229 230
      # 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

231
      def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
232
        Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
233 234
      end

235
      # Must return the MySQL error number from the exception, if the exception has an
236 237 238 239 240 241 242
      # error number.
      def error_number(exception) # :nodoc:
        raise NotImplementedError
      end

      # QUOTING ==================================================

243 244 245
      def _quote(value) # :nodoc:
        if value.is_a?(Type::Binary::Data)
          "x'#{value.hex}'"
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
        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

263 264 265 266
      def unquoted_true
        1
      end

267 268 269 270
      def quoted_false
        QUOTED_FALSE
      end

271 272 273 274
      def unquoted_false
        0
      end

275 276
      # REFERENTIAL INTEGRITY ====================================

R
Rafael Mendonça França 已提交
277
      def disable_referential_integrity #:nodoc:
278 279 280 281 282 283 284 285 286 287
        old = select_value("SELECT @@FOREIGN_KEY_CHECKS")

        begin
          update("SET FOREIGN_KEY_CHECKS = 0")
          yield
        ensure
          update("SET FOREIGN_KEY_CHECKS = #{old}")
        end
      end

288
      #--
289
      # DATABASE STATEMENTS ======================================
290
      #++
291

292 293 294 295 296
      def clear_cache!
        super
        reload_type_map
      end

297 298
      # Executes the SQL statement in the context of this connection.
      def execute(sql, name = nil)
299
        log(sql, name) { @connection.query(sql) }
300 301 302
      end

      # MysqlAdapter has to free a result after using it, so we use this method to write
303
      # stuff in an abstract way without concerning ourselves about whether it needs to be
304 305 306 307 308 309 310 311 312 313 314 315 316 317
      # 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"
      end

318 319 320 321 322
      def begin_isolated_db_transaction(isolation)
        execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
        begin_db_transaction
      end

323 324 325 326 327 328 329 330 331 332
      def commit_db_transaction #:nodoc:
        execute "COMMIT"
      end

      def rollback_db_transaction #:nodoc:
        execute "ROLLBACK"
      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
333
      # these, we must use a subquery.
334 335
      def join_to_update(update, select) #:nodoc:
        if select.limit || select.offset || select.orders.any?
336
          super
337 338 339 340 341 342
        else
          update.table select.source
          update.wheres = select.constraints
        end
      end

J
Jon Leighton 已提交
343 344 345 346
      def empty_insert_statement_value
        "VALUES ()"
      end

347 348 349 350 351 352
      # SCHEMA STATEMENTS ========================================

      # Drops the database specified on the +name+ attribute
      # and creates it again using the provided +options+.
      def recreate_database(name, options = {})
        drop_database(name)
353
        sql = create_database(name, options)
354
        reconnect!
355
        sql
356 357 358 359 360 361
      end

      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
      # Charset defaults to utf8.
      #
      # Example:
A
AvnerCohen 已提交
362
      #   create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
363
      #   create_database 'matt_development'
A
AvnerCohen 已提交
364
      #   create_database 'matt_development', charset: :big5
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
      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

395 396
      def tables(name = nil, database = nil, like = nil) #:nodoc:
        sql = "SHOW TABLES "
397
        sql << "IN #{quote_table_name(database)} " if database
398
        sql << "LIKE #{quote(like)}" if like
399 400

        execute_and_free(sql, 'SCHEMA') do |result|
401
          result.collect(&:first)
402 403 404
        end
      end

405 406 407 408
      def truncate(table_name, name = nil)
        execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
      end

409
      def table_exists?(name)
410
        return false unless name.present?
411
        return true if tables(nil, nil, name).any?
412 413 414 415 416 417 418 419 420

        name          = name.to_s
        schema, table = name.split('.', 2)

        unless table # A table was provided without a schema
          table  = schema
          schema = nil
        end

421
        tables(nil, schema, table).any?
422 423 424 425 426 427 428 429 430 431 432
      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]
433 434 435 436 437

              mysql_index_type = row[:Index_type].downcase.to_sym
              index_type  = INDEX_TYPES.include?(mysql_index_type)  ? mysql_index_type : nil
              index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
              indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
438 439 440 441 442 443 444 445 446 447 448
            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+.
449
      def columns(table_name)#:nodoc:
450
        sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
451 452
        execute_and_free(sql, 'SCHEMA') do |result|
          each_hash(result).map do |field|
453
            field_name = set_field_encoding(field[:Field])
454 455 456
            sql_type = field[:Type]
            cast_type = lookup_cast_type(sql_type)
            new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
457 458 459 460 461 462 463 464
          end
        end
      end

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

465
      def bulk_change_table(table_name, operations) #:nodoc:
466
        sqls = operations.flat_map do |command, args|
467 468 469
          table, arguments = args.shift, args
          method = :"#{command}_sql"

470
          if respond_to?(method, true)
471 472 473 474
            send(method, table, *arguments)
          else
            raise "Unknown method called : #{method}(#{arguments.inspect})"
          end
475
        end.join(", ")
476 477 478 479

        execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
      end

480 481 482 483 484 485
      # 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)}"
486
        rename_table_indexes(table_name, new_name)
487 488
      end

489
      def drop_table(table_name, options = {})
490
        execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
491 492
      end

493
      def rename_index(table_name, old_name, new_name)
494
        if supports_rename_index?
495 496 497 498 499 500
          execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
        else
          super
        end
      end

T
Tony Miller 已提交
501
      def change_column_default(table_name, column_name, default) #:nodoc:
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
        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)}")
522
        rename_column_indexes(table_name, column_name, new_column_name)
523 524
      end

D
doabit 已提交
525
      def add_index(table_name, column_name, options = {}) #:nodoc:
526 527
        index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
D
doabit 已提交
528 529
      end

530
      def foreign_keys(table_name)
531
        fk_info = select_all <<-SQL.strip_heredoc
532 533 534 535 536 537 538 539
          SELECT fk.referenced_table_name as 'to_table'
                ,fk.referenced_column_name as 'primary_key'
                ,fk.column_name as 'column'
                ,fk.constraint_name as 'name'
          FROM information_schema.key_column_usage fk
          WHERE fk.referenced_column_name is not null
            AND fk.table_schema = '#{@config[:database]}'
            AND fk.table_name = '#{table_name}'
540
        SQL
541 542 543 544

        create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]

        fk_info.map do |row|
545 546 547 548 549 550
          options = {
            column: row['column'],
            name: row['name'],
            primary_key: row['primary_key']
          }

Y
Yves Senn 已提交
551 552
          options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
          options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
553

554 555 556 557
          ForeignKeyDefinition.new(table_name, row['to_table'], options)
        end
      end

558 559
      # Maps logical Rails types to MySQL-specific data types.
      def type_to_sql(type, limit = nil, precision = nil, scale = nil)
560
        case type.to_s
561 562 563 564 565 566 567
        when 'binary'
          case limit
          when 0..0xfff;           "varbinary(#{limit})"
          when nil;                "blob"
          when 0x1000..0xffffffff; "blob(#{limit})"
          else raise(ActiveRecordError, "No binary type has character length #{limit}")
          end
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
        when '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
        when 'text'
          case limit
          when 0..0xff;               'tinytext'
          when nil, 0x100..0xffff;    'text'
          when 0x10000..0xffffff;     'mediumtext'
          when 0x1000000..0xffffffff; 'longtext'
          else raise(ActiveRecordError, "No text type has character length #{limit}")
          end
        else
          super
587 588 589 590 591
        end
      end

      # SHOW VARIABLES LIKE 'name'
      def show_variable(name)
K
kennyj 已提交
592
        variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
593 594 595 596 597
        variables.first['Value'] unless variables.empty?
      end

      # Returns a table's primary key and belonging sequence.
      def pk_and_sequence_for(table)
K
kennyj 已提交
598 599
        execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
          create_table = each_hash(result).first[:"Create Table"]
600
          if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
S
Seamus Abshere 已提交
601
            keys = $1.split(",").map { |key| key.delete('`"') }
K
kennyj 已提交
602 603 604 605
            keys.length == 1 ? [keys.first, nil] : nil
          else
            nil
          end
606 607 608 609 610 611 612 613 614
        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

615 616
      def case_sensitive_modifier(node, table_attribute)
        node = Arel::Nodes.build_quoted node, table_attribute
617 618 619
        Arel::Nodes::Bin.new(node)
      end

620 621 622 623 624 625 626 627
      def case_sensitive_comparison(table, attribute, column, value)
        if column.case_sensitive?
          table[attribute].eq(value)
        else
          super
        end
      end

628 629 630 631 632 633 634 635
      def case_insensitive_comparison(table, attribute, column, value)
        if column.case_sensitive?
          super
        else
          table[attribute].eq(value)
        end
      end

636
      def strict_mode?
637
        self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
638 639
      end

640 641 642 643
      def valid_type?(type)
        !native_database_types[type].nil?
      end

644 645
      protected

S
Sean Griffin 已提交
646
      def initialize_type_map(m) # :nodoc:
647
        super
648

649 650
        register_class_with_limit m, %r(char)i, MysqlString

651 652 653 654 655 656
        m.register_type %r(tinytext)i,   Type::Text.new(limit: 2**8 - 1)
        m.register_type %r(tinyblob)i,   Type::Binary.new(limit: 2**8 - 1)
        m.register_type %r(text)i,       Type::Text.new(limit: 2**16 - 1)
        m.register_type %r(blob)i,       Type::Binary.new(limit: 2**16 - 1)
        m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
        m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
657 658
        m.register_type %r(longtext)i,   Type::Text.new(limit: 2**32 - 1)
        m.register_type %r(longblob)i,   Type::Binary.new(limit: 2**32 - 1)
S
Sean Griffin 已提交
659 660 661
        m.register_type %r(^float)i,     Type::Float.new(limit: 24)
        m.register_type %r(^double)i,    Type::Float.new(limit: 53)

662 663 664 665 666 667
        register_integer_type m, %r(^bigint)i,    limit: 8
        register_integer_type m, %r(^int)i,       limit: 4
        register_integer_type m, %r(^mediumint)i, limit: 3
        register_integer_type m, %r(^smallint)i,  limit: 2
        register_integer_type m, %r(^tinyint)i,   limit: 1

668 669 670 671
        m.alias_type %r(tinyint\(1\))i,  'boolean' if emulate_booleans
        m.alias_type %r(set)i,           'varchar'
        m.alias_type %r(year)i,          'integer'
        m.alias_type %r(bit)i,           'binary'
672 673 674 675

        m.register_type(%r(enum)i) do |sql_type|
          limit = sql_type[/^enum\((.+)\)/i, 1]
            .split(',').map{|enum| enum.strip.length - 2}.max
676
          MysqlString.new(limit: limit)
677
        end
678 679
      end

680 681 682 683 684 685 686 687 688 689
      def register_integer_type(mapping, key, options) # :nodoc:
        mapping.register_type(key) do |sql_type|
          if /unsigned/i =~ sql_type
            Type::UnsignedInteger.new(options)
          else
            Type::Integer.new(options)
          end
        end
      end

690 691 692 693 694 695 696 697 698 699 700
      # MySQL is too stupid to create a temporary table for use subquery, so we have
      # to give it some prompting in the form of a subsubquery. Ugh!
      def subquery_for(key, select)
        subsubselect = select.clone
        subsubselect.projections = [key]

        subselect = Arel::SelectManager.new(select.engine)
        subselect.project Arel.sql(key.name)
        subselect.from subsubselect.as('__active_record_temp')
      end

701 702 703 704
      def add_index_length(option_strings, column_names, options = {})
        if options.is_a?(Hash) && length = options[:length]
          case length
          when Hash
705
            column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?}
706 707 708 709 710 711 712 713
          when Fixnum
            column_names.each {|name| option_strings[name] += "(#{length})"}
          end
        end

        return option_strings
      end

714
      def quoted_columns_for_index(column_names, options = {})
715
        option_strings = Hash[column_names.map {|name| [name, '']}]
716

717 718 719 720 721 722 723
        # add index length
        option_strings = add_index_length(option_strings, column_names, options)

        # add index sort order
        option_strings = add_index_sort_order(option_strings, column_names, options)

        column_names.map {|name| quote_column_name(name) + option_strings[name]}
724 725 726 727 728 729 730 731 732 733 734 735 736 737
      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 = {})
738 739 740
        td = create_table_definition table_name, options[:temporary], options[:options]
        cd = td.new_column_definition(column_name, type, options)
        schema_creation.visit_AddColumn cd
741 742 743 744 745 746 747 748 749 750 751 752 753
      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

754 755
        options[:name] = column.name
        schema_creation.accept ChangeColumnDefinition.new column, type, options
756 757 758
      end

      def rename_column_sql(table_name, column_name, new_column_name)
759
        column  = column_for(table_name, column_name)
760 761 762 763 764 765
        options = {
          name: new_column_name,
          default: column.default,
          null: column.null,
          auto_increment: column.extra == "auto_increment"
        }
766

K
kennyj 已提交
767
        current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
768
        schema_creation.accept ChangeColumnDefinition.new column, current_type, options
769 770
      end

M
Marc-Andre Lafortune 已提交
771 772 773 774 775 776
      def remove_column_sql(table_name, column_name, type = nil, options = {})
        "DROP #{quote_column_name(column_name)}"
      end

      def remove_columns_sql(table_name, *column_names)
        column_names.map {|column_name| remove_column_sql(table_name, column_name) }
777 778 779 780 781 782 783 784 785 786 787 788
      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

789 790
      def add_timestamps_sql(table_name, options = {})
        [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
791 792
      end

793
      def remove_timestamps_sql(table_name, options = {})
794 795 796
        [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
      end

797 798
      private

799
      def version
800
        @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
801 802 803
      end

      def mariadb?
804
        full_version =~ /mariadb/i
805 806 807 808 809 810
      end

      def supports_rename_index?
        mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
      end

811
      def configure_connection
812
        variables = @config.fetch(:variables, {}).stringify_keys
813 814 815

        # By default, MySQL 'where id is null' selects the last inserted id.
        # Turn this off. http://dev.rubyonrails.org/ticket/6778
816
        variables['sql_auto_is_null'] = 0
817 818 819 820

        # Increase timeout so the server doesn't disconnect us.
        wait_timeout = @config[:wait_timeout]
        wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
821
        variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
822 823 824 825

        # Make MySQL reject illegal values rather than truncating or blanking them, see
        # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
        # If the user has provided another value for sql_mode, don't replace it.
826 827
        unless variables.has_key?('sql_mode')
          variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
828 829 830 831 832
        end

        # NAMES does not have an equals sign, see
        # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
        # (trailing comma because variable_assignments will always have content)
833 834 835 836 837
        if @config[:encoding]
          encoding = "NAMES #{@config[:encoding]}"
          encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
          encoding << ", "
        end
838 839 840 841

        # Gather up all of the SET variables...
        variable_assignments = variables.map do |k, v|
          if v == ':default' || v == :default
842
            "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
843
          elsif !v.nil?
844
            "@@SESSION.#{k} = #{quote(v)}"
845 846 847 848 849
          end
          # or else nil; compact to clear nils out
        end.compact.join(', ')

        # ...and send them all in one query
850
        @connection.query  "SET #{encoding} #{variable_assignments}"
851
      end
852 853 854 855 856 857 858 859 860

      def extract_foreign_key_action(structure, name, action) # :nodoc:
        if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
          case $1
          when 'CASCADE'; :cascade
          when 'SET NULL'; :nullify
          end
        end
      end
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880

      class MysqlString < Type::String # :nodoc:
        def type_cast_for_database(value)
          case value
          when true then "1"
          when false then "0"
          else super
          end
        end

        private

        def cast_value(value)
          case value
          when true then "1"
          when false then "0"
          else super
          end
        end
      end
881 882 883
    end
  end
end