Flowdock
method

default_uniqueness_comparison

Importance_0
v6.0.0 - Show latest stable - 0 notes - Class: AbstractMysqlAdapter
  • 1.0.0
  • 1.1.6
  • 1.2.6
  • 2.0.3
  • 2.1.0
  • 2.2.1
  • 2.3.8
  • 3.0.0
  • 3.0.9
  • 3.1.0
  • 3.2.1
  • 3.2.8
  • 3.2.13
  • 4.0.2
  • 4.1.8
  • 4.2.1
  • 4.2.7
  • 4.2.9
  • 5.0.0.1
  • 5.1.7
  • 5.2.3
  • 6.0.0 (0)
  • 6.1.3.1
  • 6.1.7.7
  • 7.0.0
  • 7.1.3.2
  • What's this?
default_uniqueness_comparison(attribute, value, klass) public

No documentation

This method has no description. You can help the Ruby on Rails community by adding new notes.

Hide source
# File activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb, line 451
      def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
        column = column_for_attribute(attribute)

        if column.collation && !column.case_sensitive? && !value.nil?
          ActiveSupport::Deprecation.warn(<<~MSG.squish)
            Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
            To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
            pass `case_sensitive: true` option explicitly to the uniqueness validator.
          MSG
          attribute.eq(Arel::Nodes::Bin.new(value))
        else
          super
        end
      end

      def case_sensitive_comparison(attribute, value) # :nodoc:
        column = column_for_attribute(attribute)

        if column.collation && !column.case_sensitive?
          attribute.eq(Arel::Nodes::Bin.new(value))
        else
          super
        end
      end

      def can_perform_case_insensitive_comparison_for?(column)
        column.case_sensitive?
      end
      private :can_perform_case_insensitive_comparison_for?

      # 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
          s.gsub(/\s+(?:ASC|DESC)\b/, "")
        }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }

        (order_columns << super).join(", ")
      end

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

      def default_index_type?(index) # :nodoc:
        index.using == :btree || super
      end

      def build_insert_sql(insert) # :nodoc:
        sql = +"INSERT #{insert.into} #{insert.values_list}"

        if insert.skip_duplicates?
          no_op_column = quote_column_name(insert.keys.first)
          sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
        elsif insert.update_duplicates?
          sql << " ON DUPLICATE KEY UPDATE "
          sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
        end

        sql
      end

      def check_version # :nodoc:
        if database_version < "5.5.8"
          raise "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
        end
      end

      private

        def initialize_type_map(m = type_map)
          super

          register_class_with_limit m, %(char), MysqlString

          m.register_type %(tinytext),   Type::Text.new(limit: 2**8 - 1)
          m.register_type %(tinyblob),   Type::Binary.new(limit: 2**8 - 1)
          m.register_type %(text),       Type::Text.new(limit: 2**16 - 1)
          m.register_type %(blob),       Type::Binary.new(limit: 2**16 - 1)
          m.register_type %(mediumtext), Type::Text.new(limit: 2**24 - 1)
          m.register_type %(mediumblob), Type::Binary.new(limit: 2**24 - 1)
          m.register_type %(longtext),   Type::Text.new(limit: 2**32 - 1)
          m.register_type %(longblob),   Type::Binary.new(limit: 2**32 - 1)
          m.register_type %(^float),     Type::Float.new(limit: 24)
          m.register_type %(^double),    Type::Float.new(limit: 53)

          register_integer_type m, %(^bigint),    limit: 8
          register_integer_type m, %(^int),       limit: 4
          register_integer_type m, %(^mediumint), limit: 3
          register_integer_type m, %(^smallint),  limit: 2
          register_integer_type m, %(^tinyint),   limit: 1

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

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

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

        def register_integer_type(mapping, key, options)
          mapping.register_type(key) do |sql_type|
            if /\bunsigned\b/.match?(sql_type)
              Type::UnsignedInteger.new(options)
            else
              Type::Integer.new(options)
            end
          end
        end

        def extract_precision(sql_type)
          if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
            super || 0
          else
            super
          end
        end

        # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
        ER_DUP_ENTRY            = 1062
        ER_NOT_NULL_VIOLATION   = 1048
        ER_NO_REFERENCED_ROW    = 1216
        ER_ROW_IS_REFERENCED    = 1217
        ER_DO_NOT_HAVE_DEFAULT  = 1364
        ER_ROW_IS_REFERENCED_2  = 1451
        ER_NO_REFERENCED_ROW_2  = 1452
        ER_DATA_TOO_LONG        = 1406
        ER_OUT_OF_RANGE         = 1264
        ER_LOCK_DEADLOCK        = 1213
        ER_CANNOT_ADD_FOREIGN   = 1215
        ER_CANNOT_CREATE_TABLE  = 1005
        ER_LOCK_WAIT_TIMEOUT    = 1205
        ER_QUERY_INTERRUPTED    = 1317
        ER_QUERY_TIMEOUT        = 3024
        ER_FK_INCOMPATIBLE_COLUMNS = 3780

        def translate_exception(exception, message,, sql,, binds))
          case error_number(exception)
          when ER_DUP_ENTRY
            RecordNotUnique.new(message, sql: sql, binds: binds)
          when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
            InvalidForeignKey.new(message, sql: sql, binds: binds)
          when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS
            mismatched_foreign_key(message, sql: sql, binds: binds)
          when ER_CANNOT_CREATE_TABLE
            if message.include?("errno: 150")
              mismatched_foreign_key(message, sql: sql, binds: binds)
            else
              super
            end
          when ER_DATA_TOO_LONG
            ValueTooLong.new(message, sql: sql, binds: binds)
          when ER_OUT_OF_RANGE
            RangeError.new(message, sql: sql, binds: binds)
          when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
            NotNullViolation.new(message, sql: sql, binds: binds)
          when ER_LOCK_DEADLOCK
            Deadlocked.new(message, sql: sql, binds: binds)
          when ER_LOCK_WAIT_TIMEOUT
            LockWaitTimeout.new(message, sql: sql, binds: binds)
          when ER_QUERY_TIMEOUT
            StatementTimeout.new(message, sql: sql, binds: binds)
          when ER_QUERY_INTERRUPTED
            QueryCanceled.new(message, sql: sql, binds: binds)
          else
            super
          end
        end

        def change_column_for_alter(table_name, column_name, type, options = {})
          column = column_for(table_name, column_name)
          type ||= column.sql_type

          unless options.key?(:default)
            options[:default] = column.default
          end

          unless options.key?(:null)
            options[:null] = column.null
          end

          unless options.key?(:comment)
            options[:comment] = column.comment
          end

          td = create_table_definition(table_name)
          cd = td.new_column_definition(column.name, type, options)
          schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
        end

        def rename_column_for_alter(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?
          }

          current_type = exec_query("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}", "SCHEMA").first["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

        def add_index_for_alter(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

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

        def add_timestamps_for_alter(table_name, options = {})
          options[:null] = false if options[:null].nil?

          if !options.key?(:precision) && supports_datetime_with_precision?
            options[:precision] = 6
          end

          [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
        end

        def remove_timestamps_for_alter(table_name, options = {})
          [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
        end

        def supports_rename_index?
          mariadb? ? false : database_version >= "5.7.6"
        end

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

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

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

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

          # Make MySQL reject illegal values rather than truncating or blanking them, see
          # https://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')"
          end
          sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode

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

          # 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(", ")

          # ...and send them all in one query
          execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
        end

        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
        end

        def create_table_info(table_name) # :nodoc:
          exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", "SCHEMA").first["Create Table"]
        end

        def arel_visitor
          Arel::Visitors::MySQL.new(self)
        end

        def build_statement_pool
          StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
        end

        def mismatched_foreign_key(message, sql,, binds))
          match = /
            (?:CREATE|ALTER)\s+TABLE\s*(?:`?\w+`?\.)?`?(?<table>\w+)`?.+?
            FOREIGN\s+KEY\s*\(`?(?<foreign_key>\w+)`?\)\s*
            REFERENCES\s*(`?(?<target_table>\w+)`?)\s*\(`?(?<primary_key>\w+)`?\)
          /mi.match(sql)

          options = {
            message: message,
            sql: sql,
            binds: binds,
          }

          if match
            options[:table] = match[:table]
            options[:foreign_key] = match[:foreign_key]
            options[:target_table] = match[:target_table]
            options[:primary_key] = match[:primary_key]
            options[:primary_key_column] = column_for(match[:target_table], match[:primary_key])
          end

          MismatchedForeignKey.new(options)
        end

        def version_string(full_version_string)
          full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
        end

        class MysqlString < Type::String # :nodoc:
          def serialize(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

        ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
        ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
    end
  end
Register or log in to add new notes.