aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
diff options
context:
space:
mode:
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.rb844
1 files changed, 433 insertions, 411 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 8751b6da4b..3bee0e2c62 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,27 +1,33 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/mysql/column'
-require 'active_record/connection_adapters/mysql/explain_pretty_printer'
-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
class AbstractMysqlAdapter < AbstractAdapter
+ include MySQL::Quoting
include MySQL::ColumnDumper
- include Savepoints
def update_table_definition(table_name, base) # :nodoc:
MySQL::Table.new(table_name, base)
end
- def schema_creation
+ def schema_creation # :nodoc:
MySQL::SchemaCreation.new(self)
end
+ def arel_visitor # :nodoc:
+ Arel::Visitors::MySQL.new(self)
+ end
+
##
# :singleton-method:
# By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt>
@@ -32,19 +38,17 @@ module ActiveRecord
class_attribute :emulate_booleans
self.emulate_booleans = true
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
-
NATIVE_DATABASE_TYPES = {
primary_key: "int auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
- text: { name: "text" },
+ text: { name: "text", limit: 65535 },
integer: { name: "int", limit: 4 },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
time: { name: "time" },
date: { name: "date" },
- binary: { name: "blob" },
+ binary: { name: "blob", limit: 65535 },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}
@@ -52,36 +56,38 @@ module ActiveRecord
INDEX_TYPES = [:fulltext, :spatial]
INDEX_USINGS = [:btree, :hash]
+ class StatementPool < ConnectionAdapters::StatementPool
+ private def dealloc(stmt)
+ stmt[:stmt].close
+ end
+ end
+
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
- @quoted_column_names, @quoted_table_names = {}, {}
- @visitor = Arel::Visitors::MySQL.new self
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
-
- if version < '5.0.0'
+ 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."
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
- def version
+ def version #:nodoc:
@version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
end
+ def mariadb? # :nodoc:
+ /mariadb/i.match?(full_version)
+ end
+
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
@@ -95,6 +101,12 @@ module ActiveRecord
true
end
+ # Returns true, since this connection adapter supports prepared statement
+ # caching.
+ def supports_statement_cache?
+ true
+ end
+
# Technically MySQL allows to create indexes with the sort order syntax
# but at the moment (5.5) it doesn't yet implement them
def supports_index_sort_order?
@@ -122,7 +134,11 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- version >= '5.6.4'
+ if mariadb?
+ version >= "5.3.0"
+ else
+ version >= "5.6.4"
+ end
end
def supports_advisory_locks?
@@ -130,11 +146,11 @@ module ActiveRecord
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
- select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1'
+ select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1
end
def release_advisory_lock(lock_name) # :nodoc:
- select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1'
+ select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1
end
def native_database_types
@@ -142,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 ===========================================
@@ -153,8 +169,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation)
+ def new_column(*args) #:nodoc:
+ MySQL::Column.new(*args)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -163,48 +179,6 @@ module ActiveRecord
raise NotImplementedError
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
- end
-
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
- end
-
- def quoted_true
- QUOTED_TRUE
- end
-
- def unquoted_true
- 1
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
- def unquoted_false
- 0
- end
-
- def quoted_date(value)
- if supports_datetime_with_precision?
- super
- else
- super.sub(/\.\d{6}\z/, '')
- end
- end
-
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -218,6 +192,14 @@ module ActiveRecord
end
end
+ # CONNECTION MANAGEMENT ====================================
+
+ # Clears the prepared statements cache.
+ def clear_cache!
+ reload_type_map
+ @statements.clear
+ end
+
#--
# DATABASE STATEMENTS ======================================
#++
@@ -225,20 +207,19 @@ 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)
end
- def clear_cache!
- super
- reload_type_map
- end
-
# 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
@@ -301,9 +282,9 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
end
end
@@ -312,21 +293,21 @@ module ActiveRecord
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
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:
@@ -349,7 +330,7 @@ module ActiveRecord
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)
@@ -370,49 +351,47 @@ module ActiveRecord
def data_source_exists?(table_name)
return false unless table_name.present?
- schema, name = table_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A table was provided without a schema
+ schema, name = extract_schema_qualified_name(table_name)
sql = "SELECT table_name FROM information_schema.tables "
sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
- select_values(sql, 'SCHEMA').any?
+ select_values(sql, "SCHEMA").any?
end
def views # :nodoc:
- select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
+ select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA")
end
def view_exists?(view_name) # :nodoc:
return false unless view_name.present?
- schema, name = view_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A view was provided without a schema
+ schema, name = extract_schema_qualified_name(view_name)
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
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
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)
+ 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
@@ -421,21 +400,31 @@ module ActiveRecord
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) # :nodoc:
- sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
- execute_and_free(sql, 'SCHEMA') do |result|
- each_hash(result).map do |field|
- type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
- if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
- new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation])
- else
- new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
- end
+ 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)
end
end
- def create_table(table_name, options = {}) #:nodoc:
- super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
+ def table_comment(table_name) # :nodoc:
+ schema, name = extract_schema_qualified_name(table_name)
+
+ select_value(<<-SQL.strip_heredoc, "SCHEMA")
+ SELECT table_comment
+ FROM information_schema.tables
+ 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)
end
def bulk_change_table(table_name, operations) #:nodoc:
@@ -478,7 +467,6 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
- create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -495,7 +483,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:
@@ -505,7 +493,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:
@@ -518,75 +506,98 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, _, 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_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ execute add_sql_comment!(sql, comment)
+ end
+
+ def add_sql_comment!(sql, comment) # :nodoc:
+ sql << " COMMENT #{quote(comment)}" if comment.present?
+ sql
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'
+ raise ArgumentError unless table_name.present?
+
+ schema, name = extract_schema_qualified_name(table_name)
+
+ 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',
+ fk.constraint_name AS 'name',
+ rc.update_rule AS 'on_update',
+ rc.delete_rule AS 'on_delete'
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}'
+ JOIN information_schema.referential_constraints rc
+ USING (constraint_schema, constraint_name)
+ WHERE fk.referenced_column_name IS NOT NULL
+ AND fk.table_schema = #{quote(schema)}
+ AND fk.table_name = #{quote(name)}
SQL
- create_table_info = create_table_info(table_name)
-
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(create_table_info, row['name'], "UPDATE")
- options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "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')
+ raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+
+ table_options[:options] = raw_table_options
+
+ # strip COMMENT
+ if raw_table_options.sub!(/ COMMENT='.+'/, "")
+ table_options[:comment] = table_comment(table_name)
+ end
+
+ 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)
- variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
- variables.first['Value'] unless variables.empty?
+ select_value("SELECT @@#{name}", "SCHEMA")
rescue ActiveRecord::StatementInvalid
nil
end
@@ -594,10 +605,9 @@ module ActiveRecord
def primary_keys(table_name) # :nodoc:
raise ArgumentError unless table_name.present?
- schema, name = table_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A table was provided without a schema
+ 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'
@@ -608,20 +618,17 @@ module ActiveRecord
end
def case_sensitive_comparison(table, attribute, column, value)
- if value.nil? || column.case_sensitive?
- super
- else
+ if column.collation && !column.case_sensitive?
table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
+ else
+ super
end
end
- def case_insensitive_comparison(table, attribute, column, value)
- if column.case_sensitive?
- super
- else
- table[attribute].eq(Arel::Nodes::BindParam.new)
- 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
@@ -632,10 +639,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?
@@ -648,318 +655,333 @@ module ActiveRecord
protected
- def initialize_type_map(m) # :nodoc:
- super
-
- register_class_with_limit m, %r(char)i, MysqlString
+ def initialize_type_map(m) # :nodoc:
+ 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.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
- m.alias_type %r(year)i, 'integer'
- m.alias_type %r(bit)i, 'binary'
+ def register_integer_type(mapping, key, options) # :nodoc:
+ mapping.register_type(key) do |sql_type|
+ if /\bunsigned\z/.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
+ 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 Fixnum
- 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_NO_REFERENCED_ROW_2 = 1452
+ ER_DATA_TOO_LONG = 1406
+ ER_LOCK_DEADLOCK = 1213
+
+ 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
+ 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
- def translate_exception(exception, message)
- case error_number(exception)
- when 1062
- RecordNotUnique.new(message)
- when 1452
- InvalidForeignKey.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_column_sql(table_name, column_name, type = nil, options = {})
- "DROP #{quote_column_name(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_columns_sql(table_name, *column_names)
+ column_names.map { |column_name| remove_column_sql(table_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 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_index_sql(table_name, options = {})
- index_name = index_name_for_remove(table_name, options)
- "DROP INDEX #{index_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_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 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, options = {})
- [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
- end
+ def remove_timestamps_sql(table_name, options = {})
+ [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!
- def subquery_for(key, select)
- subsubselect = select.clone
- subsubselect.projections = [key]
+ # 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]
- # 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?
+ # 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?
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(key.name)
- subselect.from subsubselect.as('__active_record_temp')
- end
+ subselect = Arel::SelectManager.new(select.engine)
+ subselect.project Arel.sql(key.name)
+ subselect.from subsubselect.as("__active_record_temp")
+ end
- def mariadb?
- full_version =~ /mariadb/i
- end
+ def supports_rename_index?
+ mariadb? ? false : version >= "5.7.6"
+ end
- def supports_rename_index?
- mariadb? ? false : version >= '5.7.6'
- end
+ def configure_connection
+ variables = @config.fetch(:variables, {}).stringify_keys
- 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
- # 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)
- # 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'] = self.class.type_cast_config_to_integer(wait_timeout)
+ defaults = [":default", :default].to_set
- defaults = [':default', :default].to_set
+ # 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
- # 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.
- unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
- variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
- 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(", ")
- # 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 << ", "
+ # ...and send them all in one query
+ @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
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 column_definitions(table_name) # :nodoc:
+ execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
+ each_hash(result)
end
- # or else nil; compact to clear nils out
- end.compact.join(', ')
-
- # ...and send them all in one query
- @connection.query "SET #{encoding} #{variable_assignments}"
- end
+ 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
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when "CASCADE"; :cascade
+ when "SET NULL"; :nullify
end
end
- end
-
- def create_table_info_cache # :nodoc:
- @create_table_info_cache ||= {}
- end
-
- def create_table_info(table_name) # :nodoc:
- create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
- end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- MySQL::TableDefinition.new(name, temporary, options, as)
- end
+ def create_table_info(table_name) # :nodoc:
+ select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ 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 create_table_definition(*args) # :nodoc:
+ MySQL::TableDefinition.new(*args)
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 "1"
- when false then "0"
- 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 "1"
- when false then "0"
- 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