diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb')
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb | 563 |
1 files changed, 371 insertions, 192 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 52b0b3fe79..e9a3c26c32 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,88 +1,119 @@ require 'arel/visitors/bind_visitor' +require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include Savepoints + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def primary_key(name, type = :primary_key, options = {}) + options[:auto_increment] ||= type == :bigint + super + end + end + + class SchemaCreation < AbstractAdapter::SchemaCreation + def visit_AddColumn(o) + add_column_position!(super, column_options(o)) + end + + private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_TableDefinition(o) + name = o.name + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " + + statements = o.columns.map { |c| accept c } + statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) + + create_sql << "(#{statements.join(', ')}) " if statements.present? + create_sql << "#{o.options}" + create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql + end + + def visit_ChangeColumnDefinition(o) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + sql + end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" + end + end + + def schema_creation + SchemaCreation.new self + end + + def column_spec_for_primary_key(column) + spec = {} + if column.extra == 'auto_increment' + return unless column.limit == 8 + spec[:id] = ':bigint' + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict + attr_reader :collation, :strict, :extra - def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false) + def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") @strict = strict @collation = collation - - super(name, default, sql_type, null) + @extra = extra + super(name, default, cast_type, sql_type, null) + assert_valid_default(default) + extract_default end - def extract_default(default) + def extract_default if blob_or_text_column? - if default.blank? - null || strict ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) - nil - else - super + @default = null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(@default) + @default = nil end end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end - + def blob_or_text_column? sql_type =~ /blob/i || type == :text end - # Must return the relevant concrete adapter - def adapter - raise NotImplementedError - end - def case_sensitive? collation && !collation.match(/_ci$/) end - private - - def simplified_type(field_type) - return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end + def ==(other) + super && + collation == other.collation && + strict == other.strict && + extra == other.extra end - def extract_limit(sql_type) - case sql_type - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - when /^enum\((.+)\)/i - $1.split(',').map{|enum| enum.strip.length - 2}.max - else - super - end - end + private # MySQL misreports NOT NULL column default when none is given. # We can't detect this for columns which may have a legitimate '' @@ -94,6 +125,16 @@ module ActiveRecord def missing_default_forged_as_empty_string?(default) type != :string && !null && default == '' end + + 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 + + def attributes_for_hash + super + [collation, strict, extra] + end end ## @@ -116,23 +157,21 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :primary_key => "int(11) auto_increment PRIMARY KEY", :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "int", :limit => 4 }, :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, :boolean => { :name => "tinyint", :limit => 1 } } - class BindSubstitution < Arel::Visitors::MySQL # :nodoc: - include Arel::Visitors::BindVisitor - end + INDEX_TYPES = [:fulltext, :spatial] + INDEX_USINGS = [:btree, :hash] # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) @@ -140,17 +179,15 @@ module ActiveRecord @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} - if config.fetch(:prepared_statements) { true } - @visitor = Arel::Visitors::MySQL.new self + @visitor = Arel::Visitors::MySQL.new self + + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true else - @visitor = BindSubstitution.new self + @prepared_statements = false end end - def adapter_name #:nodoc: - self.class::ADAPTER_NAME - end - # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -160,11 +197,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -183,10 +215,26 @@ module ActiveRecord version[0] >= 5 end + def supports_indexes_in_create? + true + end + + def supports_foreign_keys? + true + end + + def supports_views? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end + def index_algorithms + { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' } + end + # HELPER METHODS =========================================== # The two drivers have slightly different ways of yielding hashes of results, so @@ -195,12 +243,11 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation) + def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -208,12 +255,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) - s = column.class.string_to_binary(value).unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end @@ -231,13 +275,21 @@ module ActiveRecord QUOTED_TRUE end + def unquoted_true + 1 + end + def quoted_false QUOTED_FALSE end + def unquoted_false + 0 + end + # REFERENTIAL INTEGRITY ==================================== - def disable_referential_integrity(&block) #:nodoc: + def disable_referential_integrity #:nodoc: old = select_value("SELECT @@FOREIGN_KEY_CHECKS") begin @@ -248,21 +300,18 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ + + def clear_cache! + super + reload_type_map + end # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - if name == :skip_logging - @connection.query(sql) - else - log(sql, name) { @connection.query(sql) } - end - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." - else - raise - end + log(sql, name) { @connection.query(sql) } end # MysqlAdapter has to free a result after using it, so we use this method to write @@ -279,39 +328,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue - # Transactions aren't supported end def begin_isolated_db_transaction(isolation) execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" begin_db_transaction - rescue - # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue - # Transactions aren't supported end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue - # Transactions aren't supported - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") end # In the simple case, MySQL allows us to place JOINs directly into the UPDATE @@ -332,25 +361,13 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def structure_dump #:nodoc: - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql, 'SCHEMA').map { |table| - table.delete('Table_type') - sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" - }.join - end - # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. def recreate_database(name, options = {}) drop_database(name) - create_database(name, options) + sql = create_database(name, options) + reconnect! + sql end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. @@ -396,12 +413,16 @@ module ActiveRecord sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| - result.collect { |field| field.first } + result.collect(&:first) end end + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + end + def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -424,7 +445,11 @@ module ActiveRecord if current_index != row[:Key_name] next if row[:Key_name] == 'PRIMARY' # skip the primary key current_index = row[:Key_name] - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], []) + + 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) end indexes.last.columns << row[:Column_name] @@ -440,7 +465,10 @@ module ActiveRecord sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation]) + field_name = set_field_encoding(field[:Field]) + sql_type = field[:Type] + cast_type = lookup_cast_type(sql_type) + new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -450,7 +478,7 @@ module ActiveRecord end def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| + sqls = operations.flat_map do |command, args| table, arguments = args.shift, args method = :"#{command}_sql" @@ -459,7 +487,7 @@ module ActiveRecord else raise "Unknown method called : #{method}(#{arguments.inspect})" end - end.flatten.join(", ") + end.join(", ") execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") end @@ -470,13 +498,24 @@ module ActiveRecord # 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)}" + rename_table_indexes(table_name, new_name) end - def add_column(table_name, column_name, type, options = {}) - execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") + def drop_table(table_name, options = {}) + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end - def change_column_default(table_name, column_name, default) + def rename_index(table_name, old_name, new_name) + if supports_rename_index? + validate_index_length!(table_name, new_name) + + 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 + + def change_column_default(table_name, column_name, default) #:nodoc: column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -497,6 +536,40 @@ module ActiveRecord 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)}") + rename_column_indexes(table_name, column_name, new_column_name) + end + + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" + end + + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + 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}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + 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") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end end # Maps logical Rails types to MySQL-specific data types. @@ -526,19 +599,18 @@ module ActiveRecord when 0x1000000..0xffffffff; 'longtext' else raise(ActiveRecordError, "No text type has character length #{limit}") end + when 'datetime' + return super unless precision + + case precision + when 0..6; "datetime(#{precision})" + else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.") + end else super end end - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') @@ -564,10 +636,19 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) + node = Arel::Nodes.build_quoted node, table_attribute Arel::Nodes::Bin.new(node) end + def case_sensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + table[attribute].eq(value) + else + super + end + end + def case_insensitive_comparison(table, attribute, column, value) if column.case_sensitive? super @@ -576,16 +657,65 @@ module ActiveRecord end end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql + def strict_mode? + self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end - def strict_mode? - @config.fetch(:strict, true) + def valid_type?(type) + !native_database_types[type].nil? end protected + 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) + + 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.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans + m.alias_type %r(set)i, 'varchar' + m.alias_type %r(year)i, 'integer' + m.alias_type %r(bit)i, 'binary' + + m.register_type(%r(datetime)i) do |sql_type| + precision = extract_precision(sql_type) + MysqlDateTime.new(precision: precision) + end + + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + MysqlString.new(limit: limit) + end + end + + 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 + # 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) @@ -634,10 +764,9 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - add_column_sql + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.visit_AddColumn cd end def change_column_sql(table_name, column_name, type, options = {}) @@ -651,26 +780,23 @@ module ActiveRecord options[:null] = column.null end - change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - change_column_sql + 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_sql(table_name, column_name, new_column_name) - options = {} - - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end + column = column_for(table_name, column_name) + options = { + default: column.default, + null: column.null, + auto_increment: column.extra == "auto_increment" + } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] - rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - rename_column_sql + 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 remove_column_sql(table_name, column_name, type = nil, options = {}) @@ -691,65 +817,118 @@ module ActiveRecord "DROP INDEX #{index_name}" end - def add_timestamps_sql(table_name) - [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + 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 - def remove_timestamps_sql(table_name) + def remove_timestamps_sql(table_name, options = {}) [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end private - def supports_views? - version[0] >= 5 + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) end - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column + def mariadb? + full_version =~ /mariadb/i + end + + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end def configure_connection - variables = @config[:variables] || {} + variables = @config.fetch(:variables, {}).stringify_keys # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variables[:sql_auto_is_null] = 0 + variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = wait_timeout + variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?(:sql_mode) - variables[:sql_mode] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) - encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding] + 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 v == ':default' || v == :default - "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default + "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? - "@@SESSION.#{k.to_s} = #{quote(v)}" + "@@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} #{variable_assignments}", :skip_logging) + @connection.query "SET #{encoding} #{variable_assignments}" end + 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 + + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + + class MysqlDateTime < Type::DateTime # :nodoc: + def type_cast_for_database(value) + if value.acts_like?(:time) && value.respond_to?(:usec) + result = super.to_s(:db) + case precision + when 1..6 + "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" + else + result + end + else + super + end + end + end + + class MysqlString < Type::String # :nodoc: + def type_cast_for_database(value) + case value + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else super + end + end + end end end end |