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 | 782 |
1 files changed, 407 insertions, 375 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 3e77b92141..fbc510bc0e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,14 +1,14 @@ -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_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" module ActiveRecord module ConnectionAdapters @@ -39,7 +39,7 @@ module ActiveRecord self.emulate_booleans = true NATIVE_DATABASE_TYPES = { - primary_key: "int auto_increment PRIMARY KEY", + primary_key: "bigint auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, text: { name: "text", limit: 65535 }, integer: { name: "int", limit: 4 }, @@ -67,16 +67,16 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if version < '5.0.0' - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." + if version < "5.1.10" + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." end end - CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32'] + CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] def internal_string_options_for_primary_key # :nodoc: super.tap { |options| - options[:collation] = collation.sub(/\A[^_]+/, 'utf8') if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) + options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) } end @@ -85,7 +85,7 @@ module ActiveRecord end def mariadb? # :nodoc: - full_version =~ /mariadb/i + /mariadb/i.match?(full_version) end # Returns true, since this connection adapter supports migrations. @@ -135,9 +135,9 @@ module ActiveRecord def supports_datetime_with_precision? if mariadb? - version >= '5.3.0' + version >= "5.3.0" else - version >= '5.6.4' + version >= "5.6.4" end end @@ -158,7 +158,7 @@ module ActiveRecord end def index_algorithms - { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' } + { default: "ALGORITHM = DEFAULT", copy: "ALGORITHM = COPY", inplace: "ALGORITHM = INPLACE" } end # HELPER METHODS =========================================== @@ -207,7 +207,7 @@ module ActiveRecord def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" start = Time.now - result = exec_query(sql, 'EXPLAIN', binds) + result = exec_query(sql, "EXPLAIN", binds) elapsed = Time.now - start MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) @@ -215,7 +215,11 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - log(sql, name) { @connection.query(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.query(sql) + end + end end # Mysql2Adapter doesn't have to free a result after using it, but we use this method @@ -293,58 +297,49 @@ module ActiveRecord end def current_database - select_value 'SELECT DATABASE() as db' + select_value "SELECT DATABASE() as db" end # Returns the database character set. def charset - show_variable 'character_set_database' + show_variable "character_set_database" end # Returns the database collation strategy. def collation - show_variable 'collation_database' + show_variable "collation_database" end - def tables(name = nil) # :nodoc: - 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 + def tables # :nodoc: + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(@config[:database])}" - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end + select_values(sql, "SCHEMA") + end - data_sources + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") end - def data_sources + def data_sources # :nodoc: sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(@config[:database])}" - select_values(sql, 'SCHEMA') + select_values(sql, "SCHEMA") end - def truncate(table_name, name = nil) - execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name - end + def table_exists?(table_name) # :nodoc: + return false unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) - def table_exists?(table_name) - # Update lib/active_record/internal_metadata.rb when this gets removed - 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 + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - data_source_exists?(table_name) + select_values(sql, "SCHEMA").any? end - def data_source_exists?(table_name) + def data_source_exists?(table_name) # :nodoc: return false unless table_name.present? schema, name = extract_schema_qualified_name(table_name) @@ -352,11 +347,7 @@ module ActiveRecord sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - select_values(sql, 'SCHEMA').any? - end - - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') + select_values(sql, "SCHEMA").any? end def view_exists?(view_name) # :nodoc: @@ -367,57 +358,66 @@ module ActiveRecord 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? + select_values(sql, "SCHEMA").any? + end + + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + indexes = [] current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| + 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 + next if row[:Key_name] == "PRIMARY" # skip the primary key current_index = row[:Key_name] 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, row[:Index_comment].presence) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], {}, nil, nil, index_type, index_using, row[:Index_comment].presence) end indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] end end indexes end - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - 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 - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) + def new_column_from_field(table_name, field) # :nodoc: + 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 + new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) end def table_comment(table_name) # :nodoc: - select_value(<<-SQL.strip_heredoc, 'SCHEMA') + schema, name = extract_schema_qualified_name(table_name) + + select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables - WHERE table_name=#{quote(table_name)} + WHERE table_schema = #{quote(schema)} + AND table_name = #{quote(name)} SQL end def create_table(table_name, **options) #:nodoc: - super(table_name, options: 'ENGINE=InnoDB', **options) + super(table_name, options: "ENGINE=InnoDB", **options) end def bulk_change_table(table_name, operations) #:nodoc: @@ -476,7 +476,7 @@ module ActiveRecord def change_column_default(table_name, column_name, default_or_changes) #:nodoc: default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, :default => default + change_column table_name, column_name, column.sql_type, default: default end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: @@ -486,7 +486,7 @@ module ActiveRecord 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 + change_column table_name, column_name, column.sql_type, null: null end def change_column(table_name, column_name, type, options = {}) #:nodoc: @@ -505,7 +505,7 @@ module ActiveRecord end def add_sql_comment!(sql, comment) # :nodoc: - sql << " COMMENT #{quote(comment)}" if comment + sql << " COMMENT #{quote(comment)}" if comment.present? sql end @@ -514,7 +514,7 @@ module ActiveRecord schema, name = extract_schema_qualified_name(table_name) - fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA') + fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', fk.referenced_column_name AS 'primary_key', fk.column_name AS 'column', @@ -531,59 +531,66 @@ module ActiveRecord fk_info.map do |row| options = { - column: row['column'], - name: row['name'], - primary_key: row['primary_key'] + column: row["column"], + name: row["name"], + primary_key: row["primary_key"] } - options[:on_update] = extract_foreign_key_action(row['on_update']) - options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:on_delete] = extract_foreign_key_action(row["on_delete"]) - ForeignKeyDefinition.new(table_name, row['to_table'], options) + ForeignKeyDefinition.new(table_name, row["to_table"], options) end end - def table_options(table_name) + def table_options(table_name) # :nodoc: + table_options = {} + create_table_info = create_table_info(table_name) # strip create_definitions and partition_options - raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + 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') + table_options[:options] = raw_table_options + # strip COMMENT - raw_table_options.sub!(/ COMMENT='.+'/, '') + if raw_table_options.sub!(/ COMMENT='.+'/, "") + table_options[:comment] = table_comment(table_name) + end - raw_table_options + table_options end # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) - 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 + 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 - else - super(type, limit, precision, scale) - end - sql << ' unsigned' if unsigned && type != :primary_key + sql << " unsigned" if unsigned && type != :primary_key sql end # SHOW VARIABLES LIKE 'name' def show_variable(name) - select_value("SELECT @@#{name}", 'SCHEMA') + select_value("SELECT @@#{name}", "SCHEMA") rescue ActiveRecord::StatementInvalid nil end @@ -593,7 +600,7 @@ module ActiveRecord schema, name = extract_schema_qualified_name(table_name) - select_values(<<-SQL.strip_heredoc, 'SCHEMA') + select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' @@ -604,7 +611,7 @@ module ActiveRecord end def case_sensitive_comparison(table, attribute, column, value) - if !value.nil? && column.collation && !column.case_sensitive? + if column.collation && !column.case_sensitive? table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) else super @@ -625,10 +632,10 @@ module ActiveRecord # 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/i, '') + s.gsub(/\s+(?:ASC|DESC)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(', ') + [super, *order_columns].join(", ") end def strict_mode? @@ -639,342 +646,367 @@ module ActiveRecord !native_database_types[type].nil? end - protected - - def initialize_type_map(m) # :nodoc: - super + private - register_class_with_limit m, %r(char)i, MysqlString + def initialize_type_map(m) + super - 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_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] + .split(",").map { |enum| enum.strip.length - 2 }.max + MysqlString.new(limit: limit) + end - 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(^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 + end - 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' + 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 - 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) + def extract_precision(sql_type) + if /time/.match?(sql_type) + super || 0 + else + super + end end - 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) + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra) end - end - 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) + def add_index_length(quoted_columns, **options) + if length = options[:length] + case length + when Hash + length = length.symbolize_keys + quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } + when Integer + quoted_columns.each { |name, column| column << "(#{length})" } + end end + + quoted_columns end - end - def extract_precision(sql_type) - if /time/ === sql_type - super || 0 - else + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) super end - end - - def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) - end - def add_index_length(option_strings, column_names, options = {}) - if options.is_a?(Hash) && length = options[:length] - case length - when Hash - column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} - when Integer - column_names.each {|name| option_strings[name] += "(#{length})"} + # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html + ER_DUP_ENTRY = 1062 + ER_NOT_NULL_VIOLATION = 1048 + ER_DO_NOT_HAVE_DEFAULT = 1364 + 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 + + 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_CANNOT_ADD_FOREIGN + mismatched_foreign_key(message) + when ER_CANNOT_CREATE_TABLE + if message.include?("errno: 150") + mismatched_foreign_key(message) + else + super + end + when ER_DATA_TOO_LONG + ValueTooLong.new(message) + when ER_OUT_OF_RANGE + RangeError.new(message) + when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT + NotNullViolation.new(message) + when ER_LOCK_DEADLOCK + Deadlocked.new(message) + else + super end end - return option_strings - end - - def quoted_columns_for_index(column_names, options = {}) - option_strings = Hash[column_names.map {|name| [name, '']}] + 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 - # add index length - option_strings = add_index_length(option_strings, column_names, options) + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) - # add index sort order - option_strings = add_index_sort_order(option_strings, column_names, options) + unless options_include_default?(options) + options[:default] = column.default + end - column_names.map {|name| quote_column_name(name) + option_strings[name]} - end + unless options.has_key?(:null) + options[:null] = column.null + end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html - ER_DUP_ENTRY = 1062 - ER_NO_REFERENCED_ROW_2 = 1452 - ER_DATA_TOO_LONG = 1406 - ER_LOCK_DEADLOCK = 1213 + unless options.key?(:comment) + options[:comment] = column.comment + end - 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 - TransactionSerializationError.new(message) - else - super + td = create_table_definition(table_name) + cd = td.new_column_definition(column.name, type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - end - - 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 - def change_column_sql(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) + 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? + } - unless options_include_default?(options) - options[:default] = column.default + 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 - unless options.has_key?(:null) - options[:null] = column.null + def remove_column_sql(table_name, column_name, type = nil, options = {}) + "DROP #{quote_column_name(column_name)}" 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_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? - } - - 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 + def remove_columns_sql(table_name, *column_names) + column_names.map { |column_name| remove_column_sql(table_name, column_name) } + end - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end + 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 - def remove_columns_sql(table_name, *column_names) - column_names.map {|column_name| remove_column_sql(table_name, column_name) } - end + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end - 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 + 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_index_sql(table_name, options = {}) - index_name = index_name_for_remove(table_name, options) - "DROP INDEX #{index_name}" - end + def remove_timestamps_sql(table_name, options = {}) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end - 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 + # 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] - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] - end + # 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? - private + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as("__active_record_temp") + 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) - subsubselect = select.clone - subsubselect.projections = [key] + def supports_rename_index? + mariadb? ? false : version >= "5.7.6" + end - # 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? + def configure_connection + variables = @config.fetch(:variables, {}).stringify_keys - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - subselect.from subsubselect.as('__active_record_temp') - end + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. + variables["sql_auto_is_null"] = 0 - def supports_rename_index? - mariadb? ? false : version >= '5.7.6' - end + # 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) - def configure_connection - variables = @config.fetch(:variables, {}).stringify_keys + defaults = [":default", :default].to_set - # 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 = @config[:wait_timeout] - wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) - 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.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 + # 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 << ", " + end - defaults = [':default', :default].to_set + # 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(", ") - # 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')" + # ...and send them all in one query + execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" end - 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 << ", " + 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 - # 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)}" + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify end - # or else nil; compact to clear nils out - end.compact.join(', ') - - # ...and send them all in one query - @connection.query "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 extract_foreign_key_action(specifier) # :nodoc: - case specifier - when 'CASCADE'; :cascade - when 'SET NULL'; :nullify + def create_table_info(table_name) # :nodoc: + select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - end - - def create_table_info(table_name) # :nodoc: - select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] - end - - def create_table_definition(*args) # :nodoc: - MySQL::TableDefinition.new(*args) - end - def extract_schema_qualified_name(string) # :nodoc: - schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) - schema, name = @config[:database], schema unless name - [schema, name] - end + def create_table_definition(*args) # :nodoc: + MySQL::TableDefinition.new(*args) + end - 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' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + def mismatched_foreign_key(message) + parts = message.scan(/`(\w+)`[ $)]/).flatten + MismatchedForeignKey.new( + self, + message: message, + table: parts[0], + foreign_key: parts[1], + target_table: parts[2], + primary_key: parts[3], + ) 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' - else raise(ActiveRecordError, "No text type has byte length #{limit}") + def extract_schema_qualified_name(string) # :nodoc: + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = @config[:database], schema unless name + [schema, name] end - end - 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}") + 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" + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") + end end - end - 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) + 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 end - end - 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 + 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 - private + 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 - def cast_value(value) - case value - when true then MySQL::Quoting::QUOTED_TRUE - when false then MySQL::Quoting::QUOTED_FALSE - else super + 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 end + + 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 end - end - ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) - ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end end |