abstract_mysql_adapter.rb 37.2 KB
Newer Older
1
require 'active_record/connection_adapters/abstract_adapter'
2
require 'active_record/connection_adapters/mysql/schema_creation'
3
require 'active_record/connection_adapters/mysql/schema_definitions'
4
require 'active_record/connection_adapters/mysql/schema_dumper'
5

6
require 'active_support/core_ext/string/strip'
7 8 9 10

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
11
      include MySQL::ColumnDumper
12 13
      include Savepoints

14
      def update_table_definition(table_name, base) # :nodoc:
15
        MySQL::Table.new(table_name, base)
16 17
      end

18
      def schema_creation
19
        MySQL::SchemaCreation.new(self)
20 21
      end

22
      class Column < ConnectionAdapters::Column # :nodoc:
23
        delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
24

S
Sean Griffin 已提交
25 26
        def initialize(*)
          super
27
          assert_valid_default(default)
28
          extract_default
29 30
        end

31 32 33
        def extract_default
          if blob_or_text_column?
            @default = null || strict ? nil : ''
S
Sean Griffin 已提交
34
          elsif missing_default_forged_as_empty_string?(default)
35
            @default = nil
36 37 38 39
          end
        end

        def has_default?
40
          return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
41 42
          super
        end
43

44 45 46
        def blob_or_text_column?
          sql_type =~ /blob/i || type == :text
        end
47

48 49 50 51
        def unsigned?
          /unsigned/ === sql_type
        end

52 53 54 55
        def case_sensitive?
          collation && !collation.match(/_ci$/)
        end

56 57 58 59
        def auto_increment?
          extra == 'auto_increment'
        end

60 61 62 63 64 65 66 67 68 69 70 71
        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
72 73 74 75 76 77

        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
S
Sean Griffin 已提交
78 79 80
      end

      class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
81
        attr_reader :extra, :strict
S
Sean Griffin 已提交
82

83
        def initialize(type_metadata, extra: "", strict: false)
S
Sean Griffin 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
          super(type_metadata)
          @type_metadata = type_metadata
          @extra = extra
          @strict = strict
        end

        def ==(other)
          other.is_a?(MysqlTypeMetadata) &&
            attributes_for_hash == other.attributes_for_hash
        end
        alias eql? ==

        def hash
          attributes_for_hash.hash
        end

        protected
101 102

        def attributes_for_hash
103
          [self.class, @type_metadata, extra, strict]
104
        end
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
      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 = {
127
        primary_key: "int auto_increment PRIMARY KEY",
128 129 130 131 132 133 134 135 136 137 138
        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 },
        json:        { name: "json" },
139 140
      }

141 142 143
      INDEX_TYPES  = [:fulltext, :spatial]
      INDEX_USINGS = [:btree, :hash]

144 145
      # FIXME: Make the first parameter more similar for the two adapters
      def initialize(connection, logger, connection_options, config)
146
        super(connection, logger, config)
147
        @quoted_column_names, @quoted_table_names = {}, {}
148

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

151
        if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
152
          @prepared_statements = true
153
          @visitor.extend(DetermineIfPreparableVisitor)
154
        else
A
Aaron Patterson 已提交
155 156 157 158
          @prepared_statements = false
        end
      end

159 160 161 162 163 164 165 166 167 168
      MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191
      CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32']
      def initialize_schema_migrations_table
        if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
          ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN)
        else
          ActiveRecord::SchemaMigration.create_table
        end
      end

169 170 171 172
      def version
        @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
      end

173 174 175 176 177 178 179 180 181
      # Returns true, since this connection adapter supports migrations.
      def supports_migrations?
        true
      end

      def supports_primary_key?
        true
      end

182 183 184 185
      def supports_bulk_alter? #:nodoc:
        true
      end

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

192 193 194 195 196
      # 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?
197
        version >= '5.0.0'
198 199
      end

200 201 202 203
      def supports_explain?
        true
      end

204 205 206 207
      def supports_indexes_in_create?
        true
      end

208 209 210 211
      def supports_foreign_keys?
        true
      end

212
      def supports_views?
213
        version >= '5.0.0'
214 215
      end

216
      def supports_datetime_with_precision?
217
        version >= '5.6.4'
218 219
      end

220 221 222 223 224 225
      # 5.0.0 definitely supports it, possibly supported by earlier versions but
      # not sure
      def supports_advisory_locks?
        version >= '5.0.0'
      end

226 227
      def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
        select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1'
228 229
      end

230 231
      def release_advisory_lock(lock_name) # :nodoc:
        select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1'
232 233
      end

234 235 236 237
      def native_database_types
        NATIVE_DATABASE_TYPES
      end

238 239 240 241
      def index_algorithms
        { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
      end

242 243 244 245 246 247 248 249
      # 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

250 251
      def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
        Column.new(field, default, sql_type_metadata, null, default_function, collation)
252 253
      end

254
      # Must return the MySQL error number from the exception, if the exception has an
255 256 257 258 259 260 261
      # error number.
      def error_number(exception) # :nodoc:
        raise NotImplementedError
      end

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

262 263 264
      def _quote(value) # :nodoc:
        if value.is_a?(Type::Binary::Data)
          "x'#{value.hex}'"
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        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

282 283 284 285
      def unquoted_true
        1
      end

286 287 288 289
      def quoted_false
        QUOTED_FALSE
      end

290 291 292 293
      def unquoted_false
        0
      end

294 295 296 297 298 299 300 301
      def quoted_date(value)
        if supports_datetime_with_precision?
          super
        else
          super.sub(/\.\d{6}\z/, '')
        end
      end

302 303
      # REFERENTIAL INTEGRITY ====================================

R
Rafael Mendonça França 已提交
304
      def disable_referential_integrity #:nodoc:
305 306 307 308 309 310 311 312 313 314
        old = select_value("SELECT @@FOREIGN_KEY_CHECKS")

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

315
      #--
316
      # DATABASE STATEMENTS ======================================
317
      #++
318

319 320 321 322 323 324 325 326 327 328
      def explain(arel, binds = [])
        sql     = "EXPLAIN #{to_sql(arel, binds)}"
        start   = Time.now
        result  = exec_query(sql, 'EXPLAIN', binds)
        elapsed = Time.now - start

        ExplainPrettyPrinter.new.pp(result, elapsed)
      end

      class ExplainPrettyPrinter # :nodoc:
329
        # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
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 355 356 357 358 359 360 361 362 363 364 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
        # MySQL shell:
        #
        #   +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
        #   | id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra       |
        #   +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
        #   |  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 |             |
        #   |  1 | SIMPLE      | posts | ALL   | NULL          | NULL    | NULL    | NULL  |    1 | Using where |
        #   +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
        #   2 rows in set (0.00 sec)
        #
        # This is an exercise in Ruby hyperrealism :).
        def pp(result, elapsed)
          widths    = compute_column_widths(result)
          separator = build_separator(widths)

          pp = []

          pp << separator
          pp << build_cells(result.columns, widths)
          pp << separator

          result.rows.each do |row|
            pp << build_cells(row, widths)
          end

          pp << separator
          pp << build_footer(result.rows.length, elapsed)

          pp.join("\n") + "\n"
        end

        private

        def compute_column_widths(result)
          [].tap do |widths|
            result.columns.each_with_index do |column, i|
              cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
              widths << cells_in_column.map(&:length).max
            end
          end
        end

        def build_separator(widths)
          padding = 1
          '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
        end

        def build_cells(items, widths)
          cells = []
          items.each_with_index do |item, i|
            item = 'NULL' if item.nil?
            justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
            cells << item.to_s.send(justifier, widths[i])
          end
          '| ' + cells.join(' | ') + ' |'
        end

        def build_footer(nrows, elapsed)
          rows_label = nrows == 1 ? 'row' : 'rows'
          "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
        end
      end

393
      def clear_cache!
394
        super
395 396 397
        reload_type_map
      end

398 399
      # Executes the SQL statement in the context of this connection.
      def execute(sql, name = nil)
400
        log(sql, name) { @connection.query(sql) }
401 402 403
      end

      # MysqlAdapter has to free a result after using it, so we use this method to write
404
      # stuff in an abstract way without concerning ourselves about whether it needs to be
405
      # explicitly freed or not.
406
      def execute_and_free(sql, name = nil) #:nodoc:
407 408 409
        yield execute(sql, name)
      end

410
      def update_sql(sql, name = nil) #:nodoc:
411 412 413 414 415 416 417 418
        super
        @connection.affected_rows
      end

      def begin_db_transaction
        execute "BEGIN"
      end

419 420 421 422 423
      def begin_isolated_db_transaction(isolation)
        execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
        begin_db_transaction
      end

424 425 426 427
      def commit_db_transaction #:nodoc:
        execute "COMMIT"
      end

428
      def exec_rollback_db_transaction #:nodoc:
429 430 431 432 433
        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
434
      # these, we must use a subquery.
435
      def join_to_update(update, select) #:nodoc:
436
        if select.limit || select.offset || select.orders.any?
437
          super
438 439 440 441 442 443
        else
          update.table select.source
          update.wheres = select.constraints
        end
      end

J
Jon Leighton 已提交
444 445 446 447
      def empty_insert_statement_value
        "VALUES ()"
      end

448 449 450 451 452 453
      # 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)
454
        sql = create_database(name, options)
455
        reconnect!
456
        sql
457 458 459 460 461 462
      end

      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
      # Charset defaults to utf8.
      #
      # Example:
A
AvnerCohen 已提交
463
      #   create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
464
      #   create_database 'matt_development'
A
AvnerCohen 已提交
465
      #   create_database 'matt_development', charset: :big5
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
      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

R
Ryuta Kamizono 已提交
496
      def tables(name = nil) # :nodoc:
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          #tables currently returns both tables and views.
          This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
          Use #data_sources instead.
        MSG

        if name
          ActiveSupport::Deprecation.warn(<<-MSG.squish)
            Passing arguments to #tables is deprecated without replacement.
          MSG
        end

        data_sources
      end

      def data_sources
M
Matt Jones 已提交
513
        sql = "SELECT table_name FROM information_schema.tables "
R
Ryuta Kamizono 已提交
514
        sql << "WHERE table_schema = #{quote(@config[:database])}"
515

R
Ryuta Kamizono 已提交
516
        select_values(sql, 'SCHEMA')
517 518
      end

519 520 521 522
      def truncate(table_name, name = nil)
        execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
      end

523
      def table_exists?(table_name)
524 525 526 527 528 529 530 531 532 533
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          #table_exists? currently checks both tables and views.
          This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
          Use #data_source_exists? instead.
        MSG

        data_source_exists?(table_name)
      end

      def data_source_exists?(table_name)
534
        return false unless table_name.present?
535

536 537
        schema, name = table_name.to_s.split('.', 2)
        schema, name = @config[:database], schema unless name # A table was provided without a schema
538

539 540
        sql = "SELECT table_name FROM information_schema.tables "
        sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
541

542
        select_values(sql, 'SCHEMA').any?
543 544
      end

545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
      def views # :nodoc:
        select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
      end

      def view_exists?(view_name) # :nodoc:
        return false unless view_name.present?

        schema, name = view_name.to_s.split('.', 2)
        schema, name = @config[:database], schema unless name # A view was provided without a schema

        sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
        sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"

        select_values(sql, 'SCHEMA').any?
      end

561 562 563 564 565 566 567 568 569
      # 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]
570 571 572 573 574

              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)
575 576 577 578 579 580 581 582 583 584 585
            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+.
586
      def columns(table_name)#:nodoc:
587
        sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
588 589
        execute_and_free(sql, 'SCHEMA') do |result|
          each_hash(result).map do |field|
590 591
            type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
            new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
592 593 594 595 596
          end
        end
      end

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

600
      def bulk_change_table(table_name, operations) #:nodoc:
601
        sqls = operations.flat_map do |command, args|
602 603 604
          table, arguments = args.shift, args
          method = :"#{command}_sql"

605
          if respond_to?(method, true)
606 607 608 609
            send(method, table, *arguments)
          else
            raise "Unknown method called : #{method}(#{arguments.inspect})"
          end
610
        end.join(", ")
611 612 613 614

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

615 616 617 618 619 620
      # 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)}"
621
        rename_table_indexes(table_name, new_name)
622 623
      end

624 625 626 627 628 629
      # Drops a table from the database.
      #
      # [<tt>:force</tt>]
      #   Set to +:cascade+ to drop dependent objects as well.
      #   Defaults to false.
      # [<tt>:if_exists</tt>]
630
      #   Set to +true+ to only drop the table if it exists.
631 632 633 634
      #   Defaults to false.
      # [<tt>:temporary</tt>]
      #   Set to +true+ to drop temporary table.
      #   Defaults to false.
635 636 637 638
      #
      # Although this command ignores most +options+ and the block if one is given,
      # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
      # In that case, +options+ and the block will be used by create_table.
639
      def drop_table(table_name, options = {})
640
        execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
641 642
      end

643
      def rename_index(table_name, old_name, new_name)
644
        if supports_rename_index?
645 646
          validate_index_length!(table_name, new_name)

647 648 649 650 651 652
          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

653 654
      def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
        default = extract_new_default_value(default_or_changes)
655 656 657 658
        column = column_for(table_name, column_name)
        change_column table_name, column_name, column.sql_type, :default => default
      end

659
      def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
        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)}")
675
        rename_column_indexes(table_name, column_name, new_column_name)
676 677
      end

D
doabit 已提交
678
      def add_index(table_name, column_name, options = {}) #:nodoc:
679 680
        index_name, index_type, index_columns, _, 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_algorithm}"
D
doabit 已提交
681 682
      end

683
      def foreign_keys(table_name)
684
        fk_info = select_all <<-SQL.strip_heredoc
685 686 687 688 689 690 691 692
          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}'
693
        SQL
694

695
        create_table_info = create_table_info(table_name)
696 697

        fk_info.map do |row|
698 699 700 701 702 703
          options = {
            column: row['column'],
            name: row['name'],
            primary_key: row['primary_key']
          }

Y
Yves Senn 已提交
704 705
          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")
706

707 708 709 710
          ForeignKeyDefinition.new(table_name, row['to_table'], options)
        end
      end

711
      def table_options(table_name)
712
        create_table_info = create_table_info(table_name)
713 714 715 716 717 718 719 720

        # strip create_definitions and partition_options
        raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip

        # strip AUTO_INCREMENT
        raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
      end

721
      # Maps logical Rails types to MySQL-specific data types.
722 723
      def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
        sql = case type.to_s
724
        when 'integer'
725
          integer_to_sql(limit)
726
        when 'text'
727
          text_to_sql(limit)
728 729 730 731 732 733 734 735
        when 'blob'
          binary_to_sql(limit)
        when 'binary'
          if (0..0xfff) === limit
            "varbinary(#{limit})"
          else
            binary_to_sql(limit)
          end
736
        else
737
          super(type, limit, precision, scale)
738
        end
739 740 741

        sql << ' unsigned' if unsigned && type != :primary_key
        sql
742 743 744 745
      end

      # SHOW VARIABLES LIKE 'name'
      def show_variable(name)
746 747 748 749
        variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
        variables.first['Value'] unless variables.empty?
      rescue ActiveRecord::StatementInvalid
        nil
750 751
      end

752 753 754 755 756 757 758 759 760 761 762 763 764 765
      def primary_keys(table_name) # :nodoc:
        raise ArgumentError unless table_name.present?

        schema, name = table_name.to_s.split('.', 2)
        schema, name = @config[:database], schema unless name # A table was provided without a schema

        select_values(<<-SQL.strip_heredoc, 'SCHEMA')
          SELECT column_name
          FROM information_schema.key_column_usage
          WHERE constraint_name = 'PRIMARY'
            AND table_schema = #{quote(schema)}
            AND table_name = #{quote(name)}
          ORDER BY ordinal_position
        SQL
766 767
      end

768 769
      def case_sensitive_modifier(node, table_attribute)
        node = Arel::Nodes.build_quoted node, table_attribute
770 771 772
        Arel::Nodes::Bin.new(node)
      end

773 774 775 776 777 778 779 780
      def case_sensitive_comparison(table, attribute, column, value)
        if column.case_sensitive?
          table[attribute].eq(value)
        else
          super
        end
      end

781 782 783 784 785 786 787 788
      def case_insensitive_comparison(table, attribute, column, value)
        if column.case_sensitive?
          super
        else
          table[attribute].eq(value)
        end
      end

789
      def strict_mode?
790
        self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
791 792
      end

793 794 795 796
      def valid_type?(type)
        !native_database_types[type].nil?
      end

797 798
      protected

S
Sean Griffin 已提交
799
      def initialize_type_map(m) # :nodoc:
800
        super
801

802 803
        register_class_with_limit m, %r(char)i, MysqlString

804 805 806 807 808 809
        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)
810 811
        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 已提交
812 813
        m.register_type %r(^float)i,     Type::Float.new(limit: 24)
        m.register_type %r(^double)i,    Type::Float.new(limit: 53)
814
        m.register_type %r(^json)i,      MysqlJson.new
S
Sean Griffin 已提交
815

816 817 818 819 820 821
        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

822 823 824
        m.alias_type %r(tinyint\(1\))i,  'boolean' if emulate_booleans
        m.alias_type %r(year)i,          'integer'
        m.alias_type %r(bit)i,           'binary'
825 826 827 828

        m.register_type(%r(enum)i) do |sql_type|
          limit = sql_type[/^enum\((.+)\)/i, 1]
            .split(',').map{|enum| enum.strip.length - 2}.max
829
          MysqlString.new(limit: limit)
830
        end
831 832 833 834 835 836

        m.register_type(%r(^set)i) do |sql_type|
          limit = sql_type[/^set\((.+)\)/i, 1]
            .split(',').map{|set| set.strip.length - 1}.sum - 1
          MysqlString.new(limit: limit)
        end
837 838
      end

839 840 841 842 843 844 845 846 847 848
      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

849
      def extract_precision(sql_type)
850
        if /time/ === sql_type
851 852 853 854 855 856
          super || 0
        else
          super
        end
      end

857 858
      def fetch_type_metadata(sql_type, extra = "")
        MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
S
Sean Griffin 已提交
859 860
      end

861 862 863 864
      def add_index_length(option_strings, column_names, options = {})
        if options.is_a?(Hash) && length = options[:length]
          case length
          when Hash
865
            column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?}
866 867 868 869 870 871 872 873
          when Fixnum
            column_names.each {|name| option_strings[name] += "(#{length})"}
          end
        end

        return option_strings
      end

874
      def quoted_columns_for_index(column_names, options = {})
875
        option_strings = Hash[column_names.map {|name| [name, '']}]
876

877 878 879 880 881 882 883
        # 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]}
884 885 886 887 888
      end

      def translate_exception(exception, message)
        case error_number(exception)
        when 1062
889
          RecordNotUnique.new(message)
890
        when 1452
891
          InvalidForeignKey.new(message)
892 893 894 895 896 897
        else
          super
        end
      end

      def add_column_sql(table_name, column_name, type, options = {})
898
        td = create_table_definition(table_name)
899
        cd = td.new_column_definition(column_name, type, options)
900
        schema_creation.accept(AddColumnDefinition.new(cd))
901 902 903 904 905 906 907 908 909 910 911 912 913
      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

914 915 916
        td = create_table_definition(table_name)
        cd = td.new_column_definition(column.name, type, options)
        schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
917 918 919
      end

      def rename_column_sql(table_name, column_name, new_column_name)
920
        column  = column_for(table_name, column_name)
921 922 923
        options = {
          default: column.default,
          null: column.null,
924
          auto_increment: column.auto_increment?
925
        }
926

K
kennyj 已提交
927
        current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
928 929 930
        td = create_table_definition(table_name)
        cd = td.new_column_definition(new_column_name, current_type, options)
        schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
931 932
      end

M
Marc-Andre Lafortune 已提交
933 934 935 936 937 938
      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) }
939 940 941
      end

      def add_index_sql(table_name, column_name, options = {})
942 943 944
        index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
        index_algorithm[0, 0] = ", " if index_algorithm.present?
        "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
945 946 947 948 949 950 951
      end

      def remove_index_sql(table_name, options = {})
        index_name = index_name_for_remove(table_name, options)
        "DROP INDEX #{index_name}"
      end

952 953
      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)]
954 955
      end

956
      def remove_timestamps_sql(table_name, options = {})
957 958 959
        [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
      end

960 961
      private

962 963 964 965 966 967
      # 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]

968 969 970 971
        # Materialize subquery by adding distinct
        # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
        subsubselect.distinct unless select.limit || select.offset || select.orders.any?

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

977
      def mariadb?
978
        full_version =~ /mariadb/i
979 980 981
      end

      def supports_rename_index?
982
        mariadb? ? false : version >= '5.7.6'
983 984
      end

985
      def configure_connection
986
        variables = @config.fetch(:variables, {}).stringify_keys
987

988
        # By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
989
        variables['sql_auto_is_null'] = 0
990 991 992 993

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

996 997
        defaults = [':default', :default].to_set

998
        # Make MySQL reject illegal values rather than truncating or blanking them, see
999
        # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
1000
        # If the user has provided another value for sql_mode, don't replace it.
1001
        unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
1002
          variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
1003 1004 1005
        end

        # NAMES does not have an equals sign, see
1006
        # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
1007
        # (trailing comma because variable_assignments will always have content)
1008 1009 1010 1011 1012
        if @config[:encoding]
          encoding = "NAMES #{@config[:encoding]}"
          encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
          encoding << ", "
        end
1013 1014 1015

        # Gather up all of the SET variables...
        variable_assignments = variables.map do |k, v|
1016
          if defaults.include?(v)
1017
            "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
1018
          elsif !v.nil?
1019
            "@@SESSION.#{k} = #{quote(v)}"
1020 1021 1022 1023 1024
          end
          # or else nil; compact to clear nils out
        end.compact.join(', ')

        # ...and send them all in one query
1025
        @connection.query  "SET #{encoding} #{variable_assignments}"
1026
      end
1027 1028 1029 1030 1031 1032 1033 1034 1035

      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
1036

1037 1038 1039 1040 1041
      def create_table_info(table_name) # :nodoc:
        @create_table_info_cache = {}
        @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
      end

1042
      def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
1043
        MySQL::TableDefinition.new(name, temporary, options, as)
1044 1045
      end

1046 1047 1048 1049 1050
      def integer_to_sql(limit) # :nodoc:
        case limit
        when 1; 'tinyint'
        when 2; 'smallint'
        when 3; 'mediumint'
1051
        when nil, 4; 'int'
1052
        when 5..8; 'bigint'
1053
        when 11; 'int(11)' # backward compatibility with Rails 2.0
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
        else raise(ActiveRecordError, "No integer type has byte size #{limit}")
        end
      end

      def text_to_sql(limit) # :nodoc:
        case limit
        when 0..0xff;               'tinytext'
        when nil, 0x100..0xffff;    'text'
        when 0x10000..0xffffff;     'mediumtext'
        when 0x1000000..0xffffffff; 'longtext'
1064
        else raise(ActiveRecordError, "No text type has byte length #{limit}")
1065 1066 1067
        end
      end

1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
      def binary_to_sql(limit) # :nodoc:
        case limit
        when 0..0xff;               'tinyblob'
        when nil, 0x100..0xffff;    'blob'
        when 0x10000..0xffffff;     'mediumblob'
        when 0x1000000..0xffffffff; 'longblob'
        else raise(ActiveRecordError, "No binary type has byte length #{limit}")
        end
      end

1078
      class MysqlJson < Type::Internal::AbstractJson # :nodoc:
1079 1080 1081 1082 1083 1084 1085
        def changed_in_place?(raw_old_value, new_value)
          # Normalization is required because MySQL JSON data format includes
          # the space between the elements.
          super(serialize(deserialize(raw_old_value)), new_value)
        end
      end

1086
      class MysqlString < Type::String # :nodoc:
1087
        def serialize(value)
1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
          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
1105

1106
      ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
1107
      ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
1108
      ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
1109 1110 1111
    end
  end
end