abstract_mysql_adapter.rb 34.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/mysql/column"
require "active_record/connection_adapters/mysql/explain_pretty_printer"
require "active_record/connection_adapters/mysql/quoting"
require "active_record/connection_adapters/mysql/schema_creation"
require "active_record/connection_adapters/mysql/schema_definitions"
require "active_record/connection_adapters/mysql/schema_dumper"
require "active_record/connection_adapters/mysql/type_metadata"

require "active_support/core_ext/string/strip"
require "active_support/core_ext/regexp"
13 14 15 16

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
17
      include MySQL::Quoting
18
      include MySQL::ColumnDumper
19

20
      def update_table_definition(table_name, base) # :nodoc:
21
        MySQL::Table.new(table_name, base)
22 23
      end

24
      def schema_creation # :nodoc:
25
        MySQL::SchemaCreation.new(self)
26 27
      end

28 29 30 31
      def arel_visitor # :nodoc:
        Arel::Visitors::MySQL.new(self)
      end

32 33
      ##
      # :singleton-method:
34
      # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt>
R
Ryuta Kamizono 已提交
35
      # as boolean. If you wish to disable this emulation you can add the following line
36 37
      # to your application.rb file:
      #
R
Ryuta Kamizono 已提交
38
      #   ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
39 40 41 42
      class_attribute :emulate_booleans
      self.emulate_booleans = true

      NATIVE_DATABASE_TYPES = {
43
        primary_key: "int auto_increment PRIMARY KEY",
44
        string:      { name: "varchar", limit: 255 },
45
        text:        { name: "text", limit: 65535 },
46 47 48 49 50 51
        integer:     { name: "int", limit: 4 },
        float:       { name: "float" },
        decimal:     { name: "decimal" },
        datetime:    { name: "datetime" },
        time:        { name: "time" },
        date:        { name: "date" },
52
        binary:      { name: "blob", limit: 65535 },
53 54
        boolean:     { name: "tinyint", limit: 1 },
        json:        { name: "json" },
55 56
      }

57 58 59
      INDEX_TYPES  = [:fulltext, :spatial]
      INDEX_USINGS = [:btree, :hash]

60
      class StatementPool < ConnectionAdapters::StatementPool
61
        private def dealloc(stmt)
62 63 64 65
          stmt[:stmt].close
        end
      end

66
      def initialize(connection, logger, connection_options, config)
67
        super(connection, logger, config)
68

V
Vipul A M 已提交
69
        @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
70

71
        if version < "5.0.0"
72 73
          raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0."
        end
A
Aaron Patterson 已提交
74 75
      end

76
      CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
77 78 79

      def internal_string_options_for_primary_key # :nodoc:
        super.tap { |options|
80
          options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
81
        }
82 83
      end

V
Vipul A M 已提交
84
      def version #:nodoc:
85 86 87
        @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
      end

V
Vipul A M 已提交
88
      def mariadb? # :nodoc:
89
        /mariadb/i.match?(full_version)
V
Vipul A M 已提交
90 91
      end

92 93 94 95 96 97 98 99 100
      # Returns true, since this connection adapter supports migrations.
      def supports_migrations?
        true
      end

      def supports_primary_key?
        true
      end

101 102 103 104
      def supports_bulk_alter? #:nodoc:
        true
      end

105 106 107 108 109 110
      # Returns true, since this connection adapter supports prepared statement
      # caching.
      def supports_statement_cache?
        true
      end

111
      # Technically MySQL allows to create indexes with the sort order syntax
112 113 114 115 116
      # but at the moment (5.5) it doesn't yet implement them
      def supports_index_sort_order?
        true
      end

117
      def supports_transaction_isolation?
118
        true
119 120
      end

121 122 123 124
      def supports_explain?
        true
      end

125 126 127 128
      def supports_indexes_in_create?
        true
      end

129 130 131 132
      def supports_foreign_keys?
        true
      end

133
      def supports_views?
134
        true
135 136
      end

137
      def supports_datetime_with_precision?
138
        if mariadb?
139
          version >= "5.3.0"
140
        else
141
          version >= "5.6.4"
142
        end
143 144
      end

145
      def supports_advisory_locks?
146
        true
147 148
      end

149
      def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
150
        select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1
151 152
      end

153
      def release_advisory_lock(lock_name) # :nodoc:
154
        select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1
155 156
      end

157 158 159 160
      def native_database_types
        NATIVE_DATABASE_TYPES
      end

161
      def index_algorithms
162
        { default: "ALGORITHM = DEFAULT", copy: "ALGORITHM = COPY", inplace: "ALGORITHM = INPLACE" }
163 164
      end

165 166 167 168 169 170 171 172
      # 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

173 174
      def new_column(*args) #:nodoc:
        MySQL::Column.new(*args)
175 176
      end

177
      # Must return the MySQL error number from the exception, if the exception has an
178 179 180 181 182 183 184
      # error number.
      def error_number(exception) # :nodoc:
        raise NotImplementedError
      end

      # REFERENTIAL INTEGRITY ====================================

R
Rafael Mendonça França 已提交
185
      def disable_referential_integrity #:nodoc:
186 187 188 189 190 191 192 193 194 195
        old = select_value("SELECT @@FOREIGN_KEY_CHECKS")

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

196 197 198 199 200 201 202 203
      # CONNECTION MANAGEMENT ====================================

      # Clears the prepared statements cache.
      def clear_cache!
        reload_type_map
        @statements.clear
      end

204
      #--
205
      # DATABASE STATEMENTS ======================================
206
      #++
207

208 209 210
      def explain(arel, binds = [])
        sql     = "EXPLAIN #{to_sql(arel, binds)}"
        start   = Time.now
211
        result  = exec_query(sql, "EXPLAIN", binds)
212 213
        elapsed = Time.now - start

214
        MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
215 216
      end

217 218
      # Executes the SQL statement in the context of this connection.
      def execute(sql, name = nil)
219
        log(sql, name) { @connection.query(sql) }
220 221
      end

R
Ryuta Kamizono 已提交
222 223 224 225
      # Mysql2Adapter doesn't have to free a result after using it, but we use this method
      # to write stuff in an abstract way without concerning ourselves about whether it
      # needs to be explicitly freed or not.
      def execute_and_free(sql, name = nil) # :nodoc:
226 227 228 229 230 231 232
        yield execute(sql, name)
      end

      def begin_db_transaction
        execute "BEGIN"
      end

233 234 235 236 237
      def begin_isolated_db_transaction(isolation)
        execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
        begin_db_transaction
      end

238 239 240 241
      def commit_db_transaction #:nodoc:
        execute "COMMIT"
      end

242
      def exec_rollback_db_transaction #:nodoc:
243 244 245 246 247
        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
248
      # these, we must use a subquery.
249
      def join_to_update(update, select, key) # :nodoc:
250
        if select.limit || select.offset || select.orders.any?
251
          super
252 253 254 255 256 257
        else
          update.table select.source
          update.wheres = select.constraints
        end
      end

J
Jon Leighton 已提交
258 259 260 261
      def empty_insert_statement_value
        "VALUES ()"
      end

262 263 264 265 266 267
      # 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)
268
        sql = create_database(name, options)
269
        reconnect!
270
        sql
271 272 273 274 275 276
      end

      # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
      # Charset defaults to utf8.
      #
      # Example:
A
AvnerCohen 已提交
277
      #   create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
278
      #   create_database 'matt_development'
A
AvnerCohen 已提交
279
      #   create_database 'matt_development', charset: :big5
280 281
      def create_database(name, options = {})
        if options[:collation]
282
          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
283
        else
284
          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
285 286 287 288 289 290 291 292
        end
      end

      # Drops a MySQL database.
      #
      # Example:
      #   drop_database('sebastian_development')
      def drop_database(name) #:nodoc:
293
        execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
294 295 296
      end

      def current_database
297
        select_value "SELECT DATABASE() as db"
298 299 300 301
      end

      # Returns the database character set.
      def charset
302
        show_variable "character_set_database"
303 304 305 306
      end

      # Returns the database collation strategy.
      def collation
307
        show_variable "collation_database"
308 309
      end

R
Ryuta Kamizono 已提交
310
      def tables(name = nil) # :nodoc:
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
        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 已提交
327
        sql = "SELECT table_name FROM information_schema.tables "
R
Ryuta Kamizono 已提交
328
        sql << "WHERE table_schema = #{quote(@config[:database])}"
329

330
        select_values(sql, "SCHEMA")
331 332
      end

333 334 335 336
      def truncate(table_name, name = nil)
        execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
      end

337
      def table_exists?(table_name)
338
        # Update lib/active_record/internal_metadata.rb when this gets removed
339 340 341 342 343 344 345 346 347 348
        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)
349
        return false unless table_name.present?
350

351
        schema, name = extract_schema_qualified_name(table_name)
352

353 354
        sql = "SELECT table_name FROM information_schema.tables "
        sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
355

356
        select_values(sql, "SCHEMA").any?
357 358
      end

359
      def views # :nodoc:
360
        select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA")
361 362 363 364 365
      end

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

366
        schema, name = extract_schema_qualified_name(view_name)
367 368 369 370

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

371
        select_values(sql, "SCHEMA").any?
372 373
      end

374 375 376 377
      # Returns an array of indexes for the given table.
      def indexes(table_name, name = nil) #:nodoc:
        indexes = []
        current_index = nil
378
        execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
379 380
          each_hash(result) do |row|
            if current_index != row[:Key_name]
381
              next if row[:Key_name] == "PRIMARY" # skip the primary key
382
              current_index = row[:Key_name]
383 384 385 386

              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
387
              indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence)
388 389 390 391 392 393 394 395 396 397 398
            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+.
399
      def columns(table_name) # :nodoc:
400 401 402 403 404 405 406 407
        table_name = table_name.to_s
        column_definitions(table_name).map do |field|
          type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
          if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
            default, default_function = nil, field[:Default]
          else
            default, default_function = field[:Default], nil
          end
408
          new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence)
409 410 411
        end
      end

412
      def table_comment(table_name) # :nodoc:
413 414
        schema, name = extract_schema_qualified_name(table_name)

415
        select_value(<<-SQL.strip_heredoc, "SCHEMA")
416
          SELECT table_comment
417
          FROM information_schema.tables
418 419
          WHERE table_schema = #{quote(schema)}
            AND table_name = #{quote(name)}
420 421 422
        SQL
      end

423
      def create_table(table_name, **options) #:nodoc:
424
        super(table_name, options: "ENGINE=InnoDB", **options)
425 426
      end

427
      def bulk_change_table(table_name, operations) #:nodoc:
428
        sqls = operations.flat_map do |command, args|
429 430 431
          table, arguments = args.shift, args
          method = :"#{command}_sql"

432
          if respond_to?(method, true)
433 434 435 436
            send(method, table, *arguments)
          else
            raise "Unknown method called : #{method}(#{arguments.inspect})"
          end
437
        end.join(", ")
438 439 440 441

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

442 443 444 445 446 447
      # 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)}"
448
        rename_table_indexes(table_name, new_name)
449 450
      end

451 452 453 454 455 456
      # 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>]
457
      #   Set to +true+ to only drop the table if it exists.
458 459 460 461
      #   Defaults to false.
      # [<tt>:temporary</tt>]
      #   Set to +true+ to drop temporary table.
      #   Defaults to false.
462 463 464 465
      #
      # 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.
466
      def drop_table(table_name, options = {})
467
        execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
468 469
      end

470
      def rename_index(table_name, old_name, new_name)
471
        if supports_rename_index?
472 473
          validate_index_length!(table_name, new_name)

474 475 476 477 478 479
          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

480 481
      def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
        default = extract_new_default_value(default_or_changes)
482
        column = column_for(table_name, column_name)
483
        change_column table_name, column_name, column.sql_type, default: default
484 485
      end

486
      def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
487 488 489 490 491 492
        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

493
        change_column table_name, column_name, column.sql_type, null: null
494 495 496 497 498 499 500 501
      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)}")
502
        rename_column_indexes(table_name, column_name, new_column_name)
503 504
      end

D
doabit 已提交
505
      def add_index(table_name, column_name, options = {}) #:nodoc:
506 507
        index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
        sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
508 509 510 511
        execute add_sql_comment!(sql, comment)
      end

      def add_sql_comment!(sql, comment) # :nodoc:
512
        sql << " COMMENT #{quote(comment)}" if comment
513
        sql
D
doabit 已提交
514 515
      end

516
      def foreign_keys(table_name)
517 518 519 520
        raise ArgumentError unless table_name.present?

        schema, name = extract_schema_qualified_name(table_name)

521
        fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA")
522 523 524 525 526 527
          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',
                 rc.update_rule AS 'on_update',
                 rc.delete_rule AS 'on_delete'
528
          FROM information_schema.key_column_usage fk
529 530 531
          JOIN information_schema.referential_constraints rc
          USING (constraint_schema, constraint_name)
          WHERE fk.referenced_column_name IS NOT NULL
532 533
            AND fk.table_schema = #{quote(schema)}
            AND fk.table_name = #{quote(name)}
534
        SQL
535 536

        fk_info.map do |row|
537
          options = {
538 539 540
            column: row["column"],
            name: row["name"],
            primary_key: row["primary_key"]
541 542
          }

543 544
          options[:on_update] = extract_foreign_key_action(row["on_update"])
          options[:on_delete] = extract_foreign_key_action(row["on_delete"])
545

546
          ForeignKeyDefinition.new(table_name, row["to_table"], options)
547 548 549
        end
      end

550 551 552
      def table_options(table_name) # :nodoc:
        table_options = {}

553
        create_table_info = create_table_info(table_name)
554 555

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

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

561 562
        table_options[:options] = raw_table_options

563
        # strip COMMENT
564
        if raw_table_options.sub!(/ COMMENT='.+'/, "")
565 566
          table_options[:comment] = table_comment(table_name)
        end
567

568
        table_options
569 570
      end

571
      # Maps logical Rails types to MySQL-specific data types.
572
      def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
        sql = \
          case type.to_s
          when "integer"
            integer_to_sql(limit)
          when "text"
            text_to_sql(limit)
          when "blob"
            binary_to_sql(limit)
          when "binary"
            if (0..0xfff) === limit
              "varbinary(#{limit})"
            else
              binary_to_sql(limit)
            end
          else
            super(type, limit, precision, scale)
          end
590

591
        sql << " unsigned" if unsigned && type != :primary_key
592
        sql
593 594 595 596
      end

      # SHOW VARIABLES LIKE 'name'
      def show_variable(name)
597
        select_value("SELECT @@#{name}", "SCHEMA")
598 599
      rescue ActiveRecord::StatementInvalid
        nil
600 601
      end

602 603 604
      def primary_keys(table_name) # :nodoc:
        raise ArgumentError unless table_name.present?

605
        schema, name = extract_schema_qualified_name(table_name)
606

607
        select_values(<<-SQL.strip_heredoc, "SCHEMA")
608 609 610 611 612 613 614
          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
615 616
      end

617
      def case_sensitive_comparison(table, attribute, column, value)
618
        if column.collation && !column.case_sensitive?
619 620 621
          table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
        else
          super
622 623 624
        end
      end

625 626
      def can_perform_case_insensitive_comparison_for?(column)
        column.case_sensitive?
627
      end
628
      private :can_perform_case_insensitive_comparison_for?
629

630 631 632 633 634 635 636 637 638
      # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use
      # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for
      # distinct queries, and requires that the ORDER BY include the distinct column.
      # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
      def columns_for_distinct(columns, orders) # :nodoc:
        order_columns = orders.reject(&:blank?).map { |s|
          # Convert Arel node to string
          s = s.to_sql unless s.is_a?(String)
          # Remove any ASC/DESC modifiers
639
          s.gsub(/\s+(?:ASC|DESC)\b/i, "")
640 641
        }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }

642
        [super, *order_columns].join(", ")
643 644
      end

645
      def strict_mode?
646
        self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
647 648
      end

649 650 651 652
      def valid_type?(type)
        !native_database_types[type].nil?
      end

653 654
      protected

655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
        def initialize_type_map(m) # :nodoc:
          super

          register_class_with_limit m, %r(char)i, MysqlString

          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)
          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)
          m.register_type %r(^float)i,     Type::Float.new(limit: 24)
          m.register_type %r(^double)i,    Type::Float.new(limit: 53)
          m.register_type %r(^json)i,      MysqlJson.new

          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

          m.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans
          m.alias_type %r(year)i,          "integer"
          m.alias_type %r(bit)i,           "binary"

          m.register_type(%r(enum)i) do |sql_type|
            limit = sql_type[/^enum\((.+)\)/i, 1]
684
              .split(",").map { |enum| enum.strip.length - 2 }.max
685
            MysqlString.new(limit: limit)
686 687
          end

688 689
          m.register_type(%r(^set)i) do |sql_type|
            limit = sql_type[/^set\((.+)\)/i, 1]
690
              .split(",").map { |set| set.strip.length - 1 }.sum - 1
691 692
            MysqlString.new(limit: limit)
          end
693 694
        end

695 696 697 698 699 700 701 702 703
        def register_integer_type(mapping, key, options) # :nodoc:
          mapping.register_type(key) do |sql_type|
            if /\bunsigned\z/ === sql_type
              Type::UnsignedInteger.new(options)
            else
              Type::Integer.new(options)
            end
          end
        end
S
Sean Griffin 已提交
704

705 706 707 708 709
        def extract_precision(sql_type)
          if /time/ === sql_type
            super || 0
          else
            super
710 711 712
          end
        end

713
        def fetch_type_metadata(sql_type, extra = "")
714
          MySQL::TypeMetadata.new(super(sql_type), extra: extra)
715
        end
716

717 718
        def add_index_length(quoted_columns, **options)
          if length = options[:length]
719 720
            case length
            when Hash
721
              quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? }
722
            when Integer
723
              quoted_columns.each { |name, column| column << "(#{length})" }
724 725
            end
          end
726

727
          quoted_columns
728
        end
729

730 731 732
        def add_options_for_index_columns(quoted_columns, **options)
          quoted_columns = add_index_length(quoted_columns, options)
          super
733
        end
734

735
        # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
        ER_DUP_ENTRY            = 1062
        ER_NO_REFERENCED_ROW_2  = 1452
        ER_DATA_TOO_LONG        = 1406
        ER_LOCK_DEADLOCK        = 1213

        def translate_exception(exception, message)
          case error_number(exception)
          when ER_DUP_ENTRY
            RecordNotUnique.new(message)
          when ER_NO_REFERENCED_ROW_2
            InvalidForeignKey.new(message)
          when ER_DATA_TOO_LONG
            ValueTooLong.new(message)
          when ER_LOCK_DEADLOCK
            Deadlocked.new(message)
          else
            super
          end
754 755
        end

756 757 758 759 760
        def add_column_sql(table_name, column_name, type, options = {})
          td = create_table_definition(table_name)
          cd = td.new_column_definition(column_name, type, options)
          schema_creation.accept(AddColumnDefinition.new(cd))
        end
761

762 763
        def change_column_sql(table_name, column_name, type, options = {})
          column = column_for(table_name, column_name)
764

765 766 767
          unless options_include_default?(options)
            options[:default] = column.default
          end
768

769 770 771
          unless options.has_key?(:null)
            options[:null] = column.null
          end
772

773 774 775 776
          td = create_table_definition(table_name)
          cd = td.new_column_definition(column.name, type, options)
          schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
        end
777

778 779 780 781 782 783 784
        def rename_column_sql(table_name, column_name, new_column_name)
          column  = column_for(table_name, column_name)
          options = {
            default: column.default,
            null: column.null,
            auto_increment: column.auto_increment?
          }
785

786 787 788 789 790
          current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", "SCHEMA")["Type"]
          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))
        end
791

792 793 794
        def remove_column_sql(table_name, column_name, type = nil, options = {})
          "DROP #{quote_column_name(column_name)}"
        end
M
Marc-Andre Lafortune 已提交
795

796
        def remove_columns_sql(table_name, *column_names)
797
          column_names.map { |column_name| remove_column_sql(table_name, column_name) }
798
        end
799

800 801 802 803 804
        def add_index_sql(table_name, column_name, options = {})
          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}"
        end
805

806 807 808 809
        def remove_index_sql(table_name, options = {})
          index_name = index_name_for_remove(table_name, options)
          "DROP INDEX #{index_name}"
        end
810

811 812 813
        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)]
        end
814

815 816 817
        def remove_timestamps_sql(table_name, options = {})
          [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
        end
818

819 820
      private

821 822
      # 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!
823 824 825
        def subquery_for(key, select)
          subsubselect = select.clone
          subsubselect.projections = [key]
826

827 828 829
          # 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?
830

831 832 833 834
          subselect = Arel::SelectManager.new(select.engine)
          subselect.project Arel.sql(key.name)
          subselect.from subsubselect.as("__active_record_temp")
        end
835

836 837 838
        def supports_rename_index?
          mariadb? ? false : version >= "5.7.6"
        end
839

840 841
        def configure_connection
          variables = @config.fetch(:variables, {}).stringify_keys
842

843 844
          # By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
          variables["sql_auto_is_null"] = 0
845

846 847 848 849
          # Increase timeout so the server doesn't disconnect us.
          wait_timeout = @config[:wait_timeout]
          wait_timeout = 2147483 unless wait_timeout.is_a?(Integer)
          variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout)
850

851
          defaults = [":default", :default].to_set
852

853 854 855 856 857 858 859 860 861 862 863 864 865 866
          # Make MySQL reject illegal values rather than truncating or blanking them, see
          # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
          # If the user has provided another value for sql_mode, don't replace it.
          if sql_mode = variables.delete("sql_mode")
            sql_mode = quote(sql_mode)
          elsif !defaults.include?(strict_mode?)
            if strict_mode?
              sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')"
            else
              sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')"
              sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')"
              sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')"
            end
            sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
867
          end
868 869 870 871 872 873 874 875 876
          sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode

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

879 880 881 882 883 884 885 886 887
          # Gather up all of the SET variables...
          variable_assignments = variables.map do |k, v|
            if defaults.include?(v)
              "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
            elsif !v.nil?
              "@@SESSION.#{k} = #{quote(v)}"
            end
            # or else nil; compact to clear nils out
          end.compact.join(", ")
888

889 890
          # ...and send them all in one query
          @connection.query  "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
891 892
        end

893 894 895 896
        def column_definitions(table_name) # :nodoc:
          execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
            each_hash(result)
          end
897
        end
898

899 900 901 902 903 904
        def extract_foreign_key_action(specifier) # :nodoc:
          case specifier
          when "CASCADE"; :cascade
          when "SET NULL"; :nullify
          end
        end
905

906 907 908
        def create_table_info(table_name) # :nodoc:
          select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
        end
909

910 911
        def create_table_definition(*args) # :nodoc:
          MySQL::TableDefinition.new(*args)
912 913
        end

914 915 916 917
        def extract_schema_qualified_name(string) # :nodoc:
          schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/)
          schema, name = @config[:database], schema unless name
          [schema, name]
918 919
        end

920 921 922 923 924 925 926
        def integer_to_sql(limit) # :nodoc:
          case limit
          when 1; "tinyint"
          when 2; "smallint"
          when 3; "mediumint"
          when nil, 4; "int"
          when 5..8; "bigint"
927
          else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.")
928
          end
929 930
        end

931 932 933 934 935 936 937 938
        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"
          else raise(ActiveRecordError, "No text type has byte length #{limit}")
          end
939 940
        end

941 942 943 944 945 946 947
        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}")
948 949 950
          end
        end

951 952 953 954 955 956 957
        class MysqlJson < Type::Internal::AbstractJson # :nodoc:
          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
958

959 960 961 962 963 964 965
        class MysqlString < Type::String # :nodoc:
          def serialize(value)
            case value
            when true then MySQL::Quoting::QUOTED_TRUE
            when false then MySQL::Quoting::QUOTED_FALSE
            else super
            end
966
          end
967 968 969 970 971 972 973 974 975 976

          private

            def cast_value(value)
              case value
              when true then MySQL::Quoting::QUOTED_TRUE
              when false then MySQL::Quoting::QUOTED_FALSE
              else super
              end
            end
977
        end
978

979 980 981
        ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
        ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
        ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
982 983 984
    end
  end
end