require 'dm-migrations/auto_migration' require 'dm-migrations/adapters/dm-do-adapter' module DataMapper module Migrations module MysqlAdapter DEFAULT_ENGINE = 'InnoDB'.freeze DEFAULT_CHARACTER_SET = 'utf8'.freeze DEFAULT_COLLATION = 'utf8_unicode_ci'.freeze MAXIMUM_CHAR_LENGTH = ((2**16) - 1) / 4 # allow room for utf8mb4 include SQL, DataObjectsAdapter # @api private def self.included(base) base.extend DataObjectsAdapter::ClassMethods base.extend ClassMethods end # @api semipublic def storage_exists?(storage_name) select('SHOW TABLES LIKE ?', storage_name).first == storage_name end # @api semipublic def field_exists?(storage_name, field) result = select("SHOW COLUMNS FROM #{quote_name(storage_name)} LIKE ?", field).first result ? result.field == field : false end module SQL # :nodoc: # private ## This cannot be private for current migrations # Allows for specification of the default storage engine to use when creating tables via # migrations. Defaults to DEFAULT_ENGINE. # # adapter = DataMapper.setup(:default, 'mysql://localhost/foo') # adapter.storage_engine = 'MyISAM' # # @api public attr_accessor :storage_engine # @api private def supports_serial? true end # @api private def supports_drop_table_if_exists? true end # @api private def schema_name # TODO: is there a cleaner way to find out the current DB we are connected to? normalized_uri.path.split('/').last end # @api private def create_table_statement(connection, model, properties) "#{super} ENGINE = #{storage_engine} CHARACTER SET #{character_set} COLLATE #{collation}" end # @api private def property_schema_hash(property) schema = super case property.dump_as when Integer.singleton_class if property.respond_to?(:min) && property.respond_to?(:max) min = property.min max = property.max schema[:primitive] = integer_column_statement(min..max) if min && max end when String.singleton_class if property.is_a?(Property::Text) schema[:primitive] = text_column_statement(property.length) schema.delete(:default) else schema[:length] ||= MAXIMUM_CHAR_LENGTH end end schema end # @api private def property_schema_statement(connection, schema) statement = super statement << ' AUTO_INCREMENT' if supports_serial? && schema[:serial] statement end # @api private def storage_engine # Don't pull the default engine via show_variable for backwards compat where it was hard # coded to InnoDB @storage_engine ||= DEFAULT_ENGINE end # @api private def character_set @character_set ||= show_variable('character_set_connection') || DEFAULT_CHARACTER_SET end # @api private def collation @collation ||= show_variable('collation_connection') || DEFAULT_COLLATION end # @api private def show_variable(name) result = select('SHOW VARIABLES LIKE ?', name).first result ? result.value.freeze : nil end # Return SQL statement for the text column # # @param [Integer] length # the max allowed length # # @return [String] # the statement to create the text column # # @api private private def text_column_statement(length) if length < 2**8 'TINYTEXT' elsif length < 2**16 'TEXT' elsif length < 2**24 'MEDIUMTEXT' elsif length < 2**32 'LONGTEXT' # http://www.postgresql.org/files/documentation/books/aw_pgsql/node90.html # Implies that PostgreSQL doesn't have a size limit on text # fields, so this param validation happens here instead of # DM::Property#initialize. else raise ArgumentError, "length of #{length} exceeds maximum size supported" end end # Return SQL statement for the integer column # # @param [Range] range # the min/max allowed integers # # @return [String] # the statement to create the integer column # # @api private private def integer_column_statement(range) format('%s(%d)%s', integer_column_type(range), integer_display_size(range), integer_statement_sign(range)) end # Return the integer column type # # Use the smallest available column type that will satisfy the # allowable range of numbers # # @param [Range] range # the min/max allowed integers # # @return [String] # the column type # # @api private private def integer_column_type(range) if range.first < 0 signed_integer_column_type(range) else unsigned_integer_column_type(range) end end # Return the signed integer column type # # @param [Range] range # the min/max allowed integers # # @return [String] # # @api private private def signed_integer_column_type(range) min = range.first max = range.last tinyint = 2**7 smallint = 2**15 integer = 2**31 mediumint = 2**23 bigint = 2**63 if min >= -tinyint && max < tinyint 'TINYINT' elsif min >= -smallint && max < smallint 'SMALLINT' elsif min >= -mediumint && max < mediumint 'MEDIUMINT' elsif min >= -integer && max < integer 'INT' elsif min >= -bigint && max < bigint 'BIGINT' else raise ArgumentError, "min #{min} and max #{max} exceeds supported range" end end # Return the unsigned integer column type # # @param [Range] range # the min/max allowed integers # # @return [String] # # @api private private def unsigned_integer_column_type(range) max = range.last if max < 2**8 'TINYINT' elsif max < 2**16 'SMALLINT' elsif max < 2**24 'MEDIUMINT' elsif max < 2**32 'INT' elsif max < 2**64 'BIGINT' else raise ArgumentError, "min #{range.first} and max #{max} exceeds supported range" end end # Return the integer column display size # # Adjust the display size to match the maximum number of # expected digits. This is more for documentation purposes # and does not affect what can actually be stored in a # specific column # # @param [Range] range # the min/max allowed integers # # @return [Integer] # the display size for the integer # # @api private private def integer_display_size(range) [range.first.to_s.length, range.last.to_s.length].max end # Return the integer sign statement # # @param [Range] range # the min/max allowed integers # # @return [String, nil] # statement if unsigned, nil if signed # # @api private private def integer_statement_sign(range) ' UNSIGNED' unless range.first < 0 end # @api private private def indexes(model) filter_indexes(model, super) end # @api private private def unique_indexes(model) filter_indexes(model, super) end # Filter out any indexes with a non index-able column in MySQL # # @api private private def filter_indexes(model, indexes) field_map = model.properties(name).field_map indexes.select do |_index_name, fields| fields.all? { |field| !field_map[field].is_a?(Property::Text) } end end end module ClassMethods # Types for MySQL databases. # # @return [Hash] types for MySQL databases. # # @api private def type_map super.merge( DateTime => {primitive: 'DATETIME'}, Time => {primitive: 'DATETIME'} ).freeze end end end end end