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 | 206 |
1 files changed, 111 insertions, 95 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 cc820036ca..01cd1e5446 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -9,7 +9,6 @@ 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" module ActiveRecord module ConnectionAdapters @@ -40,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 }, @@ -68,8 +67,8 @@ 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 @@ -216,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 @@ -307,45 +310,36 @@ module ActiveRecord 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") 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? - 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 + schema, name = extract_schema_qualified_name(table_name) - data_source_exists?(table_name) + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(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) @@ -356,10 +350,6 @@ module ActiveRecord select_values(sql, "SCHEMA").any? end - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") - end - def view_exists?(view_name) # :nodoc: return false unless view_name.present? @@ -371,6 +361,10 @@ module ActiveRecord 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: indexes = [] @@ -384,29 +378,25 @@ module ActiveRecord 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: @@ -509,7 +499,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 @@ -570,22 +560,23 @@ module ActiveRecord # 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 - binary_to_sql(limit) - end - else - super(type, limit, precision, scale) - end + 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 sql << " unsigned" if unsigned && type != :primary_key sql @@ -649,9 +640,9 @@ module ActiveRecord !native_database_types[type].nil? end - protected + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) super register_class_with_limit m, %r(char)i, MysqlString @@ -680,20 +671,20 @@ module ActiveRecord m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] - .split(",").map{|enum| enum.strip.length - 2}.max + .split(",").map { |enum| enum.strip.length - 2 }.max MysqlString.new(limit: limit) 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 + .split(",").map { |set| set.strip.length - 1 }.sum - 1 MysqlString.new(limit: limit) end end - def register_integer_type(mapping, key, options) # :nodoc: + def register_integer_type(mapping, key, options) mapping.register_type(key) do |sql_type| - if /\bunsigned\z/ === sql_type + if /\bunsigned\b/.match?(sql_type) Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -702,7 +693,7 @@ module ActiveRecord end def extract_precision(sql_type) - if /time/ === sql_type + if /time/.match?(sql_type) super || 0 else super @@ -710,39 +701,38 @@ module ActiveRecord end def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) + MySQL::TypeMetadata.new(super(sql_type), extra: extra) end - def add_index_length(option_strings, column_names, options = {}) - if options.is_a?(Hash) && length = options[:length] + def add_index_length(quoted_columns, **options) + if length = options[:length] case length when Hash - column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} + length = length.symbolize_keys + quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } when Integer - column_names.each {|name| option_strings[name] += "(#{length})"} + quoted_columns.each { |name, column| column << "(#{length})" } end end - return option_strings + quoted_columns end - def quoted_columns_for_index(column_names, options = {}) - option_strings = Hash[column_names.map {|name| [name, ""]}] - - # add index length - option_strings = add_index_length(option_strings, column_names, options) - - # add index sort order - option_strings = add_index_sort_order(option_strings, column_names, options) - - column_names.map {|name| quote_column_name(name) + option_strings[name]} + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) + super end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html + # 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) @@ -750,8 +740,20 @@ module ActiveRecord 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 @@ -776,6 +778,10 @@ module ActiveRecord 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)) @@ -800,7 +806,7 @@ module ActiveRecord end def remove_columns_sql(table_name, *column_names) - column_names.map {|column_name| remove_column_sql(table_name, column_name) } + column_names.map { |column_name| remove_column_sql(table_name, column_name) } end def add_index_sql(table_name, column_name, options = {}) @@ -822,10 +828,8 @@ module ActiveRecord [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end - private - - # 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! + # 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] @@ -893,7 +897,7 @@ module ActiveRecord end.compact.join(", ") # ...and send them all in one query - @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" end def column_definitions(table_name) # :nodoc: @@ -917,6 +921,18 @@ module ActiveRecord MySQL::TableDefinition.new(*args) end + 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 + def extract_schema_qualified_name(string) # :nodoc: schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) schema, name = @config[:database], schema unless name @@ -930,7 +946,7 @@ module ActiveRecord when 3; "mediumint" when nil, 4; "int" when 5..8; "bigint" - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") end end |