aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/mysql
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/mysql')
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb72
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb95
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb23
8 files changed, 284 insertions, 73 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 684c7042a7..bbcdc96cdc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- discard_remaining_results
+ @connection.abandon_results!
result
end
@@ -19,8 +19,28 @@ module ActiveRecord
execute(sql, name).to_a
end
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Concurrent.monotonic_time
+ result = exec_query(sql, "EXPLAIN", binds)
+ elapsed = Concurrent.monotonic_time - start
+
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
@@ -29,24 +49,30 @@ module ActiveRecord
end
def exec_query(sql, name = "SQL", binds = [], prepare: false)
- materialize_transactions
-
if without_prepared_statement?(binds)
execute_and_free(sql, name) do |result|
- ActiveRecord::Result.new(result.fields, result.to_a) if result
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
end
else
exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
- ActiveRecord::Result.new(result.fields, result.to_a) if result
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
end
end
end
def exec_delete(sql, name = nil, binds = [])
- materialize_transactions
-
if without_prepared_statement?(binds)
- execute_and_free(sql, name) { @connection.affected_rows }
+ @lock.synchronize do
+ execute_and_free(sql, name) { @connection.affected_rows }
+ end
else
exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
end
@@ -54,22 +80,31 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def execute_batch(sql, name = nil)
+ super
+ @connection.abandon_results!
+ end
+
def default_insert_value(column)
- Arel.sql("DEFAULT") unless column.auto_increment?
+ super unless column.auto_increment?
end
def last_inserted_id(result)
@connection.last_id
end
- def discard_remaining_results
- @connection.abandon_results!
- end
-
def supports_set_server_option?
@connection.respond_to?(:set_server_option)
end
+ def build_truncate_statements(*table_names)
+ if table_names.size == 1
+ super.first
+ else
+ super
+ end
+ end
+
def multi_statements_enabled?(flags)
if flags.is_a?(Array)
flags.include?("MULTI_STATEMENTS")
@@ -102,7 +137,40 @@ module ActiveRecord
end
end
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ if max_allowed_packet_reached?(sql, previous_packet)
+ total_sql_chunks << +sql
+ else
+ previous_packet << ";\n"
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError,
+ "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ true
+ else
+ (current_packet.bytesize + previous_packet.bytesize + 2) > max_allowed_packet
+ end
+ end
+
+ def max_allowed_packet
+ @max_allowed_packet ||= show_variable("max_allowed_packet")
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
index 20c3c83664..edd5ea0542 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
@@ -37,7 +37,6 @@ module ActiveRecord
end
private
-
def compute_column_widths(result)
[].tap do |widths|
result.columns.each_with_index do |column, i|
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index 75564a61d6..0069f5871c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -5,11 +5,11 @@ module ActiveRecord
module MySQL
module Quoting # :nodoc:
def quote_column_name(name)
- @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
+ self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
end
def unquoted_true
@@ -32,12 +32,49 @@ module ActiveRecord
"x'#{value.hex}'"
end
- def _type_cast(value)
- case value
- when Date, Time then value
- else super
- end
+ def column_name_matcher
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher
+ COLUMN_NAME_WITH_ORDER
end
+
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # `table_name`.`column_name` | function(one or no argument)
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\)
+ )
+ (?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # `table_name`.`column_name` | function(one or no argument)
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`)) | \w+\((?:|\g<2>)\)
+ )
+ (?:\s+ASC|\s+DESC)?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
+ private
+ def _type_cast(value)
+ case value
+ when Date, Time then value
+ else super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 82ed320617..0f5ab7562a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -7,7 +7,6 @@ module ActiveRecord
delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true
private
-
def visit_DropForeignKey(name)
"DROP FOREIGN KEY #{name}"
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index 2ed4ad16ae..d21535a709 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -4,48 +4,56 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module ColumnMethods
- def blob(*args, **options)
- args.each { |name| column(name, :blob, options) }
- end
+ extend ActiveSupport::Concern
- def tinyblob(*args, **options)
- args.each { |name| column(name, :tinyblob, options) }
- end
+ ##
+ # :method: blob
+ # :call-seq: blob(*names, **options)
- def mediumblob(*args, **options)
- args.each { |name| column(name, :mediumblob, options) }
- end
+ ##
+ # :method: tinyblob
+ # :call-seq: tinyblob(*names, **options)
- def longblob(*args, **options)
- args.each { |name| column(name, :longblob, options) }
- end
+ ##
+ # :method: mediumblob
+ # :call-seq: mediumblob(*names, **options)
- def tinytext(*args, **options)
- args.each { |name| column(name, :tinytext, options) }
- end
+ ##
+ # :method: longblob
+ # :call-seq: longblob(*names, **options)
- def mediumtext(*args, **options)
- args.each { |name| column(name, :mediumtext, options) }
- end
+ ##
+ # :method: tinytext
+ # :call-seq: tinytext(*names, **options)
- def longtext(*args, **options)
- args.each { |name| column(name, :longtext, options) }
- end
+ ##
+ # :method: mediumtext
+ # :call-seq: mediumtext(*names, **options)
- def unsigned_integer(*args, **options)
- args.each { |name| column(name, :unsigned_integer, options) }
- end
+ ##
+ # :method: longtext
+ # :call-seq: longtext(*names, **options)
- def unsigned_bigint(*args, **options)
- args.each { |name| column(name, :unsigned_bigint, options) }
- end
+ ##
+ # :method: unsigned_integer
+ # :call-seq: unsigned_integer(*names, **options)
- def unsigned_float(*args, **options)
- args.each { |name| column(name, :unsigned_float, options) }
- end
+ ##
+ # :method: unsigned_bigint
+ # :call-seq: unsigned_bigint(*names, **options)
+
+ ##
+ # :method: unsigned_float
+ # :call-seq: unsigned_float(*names, **options)
+
+ ##
+ # :method: unsigned_decimal
+ # :call-seq: unsigned_decimal(*names, **options)
- def unsigned_decimal(*args, **options)
- args.each { |name| column(name, :unsigned_decimal, options) }
+ included do
+ define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
+ :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
+ :unsigned_float, :unsigned_decimal
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index d23178e43c..bcd300f3db 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -10,6 +10,10 @@ module ActiveRecord
spec[:unsigned] = "true" if column.unsigned?
spec[:auto_increment] = "true" if column.auto_increment?
+ if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type
+ spec = { size: size.to_sym.inspect }.merge!(spec)
+ end
+
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
@@ -37,19 +41,23 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
- when "tinyblob"
- :blob
+ when /\A(?:enum|set)\b/
+ column.sql_type
else
super
end
end
+ def schema_limit(column)
+ super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type)
+ end
+
def schema_precision(column)
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
def schema_collation(column)
- if column.collation && table_name = column.table_name
+ if column.collation
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||=
@connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"]
@@ -58,14 +66,14 @@ module ActiveRecord
end
def extract_expression_for_virtual_column(column)
- if @connection.mariadb? && @connection.version < "10.2.5"
- create_table_info = @connection.send(:create_table_info, column.table_name)
+ if @connection.mariadb? && @connection.database_version < "10.2.5"
+ create_table_info = @connection.send(:create_table_info, table_name)
column_name = @connection.quote_column_name(column.name)
if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info
$~[:expression].inspect
end
else
- scope = @connection.send(:quoted_scope, column.table_name)
+ scope = @connection.send(:quoted_scope, table_name)
column_name = @connection.quote(column.name)
sql = "SELECT generation_expression FROM information_schema.columns" \
" WHERE table_schema = #{scope[:schema]}" \
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 4894fd1c08..25a1fb234a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -77,9 +77,13 @@ module ActiveRecord
super
end
+ def create_table(table_name, options: default_row_format, **)
+ super
+ end
+
def internal_string_options_for_primary_key
super.tap do |options|
- if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0")
+ if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
options[:collation] = collation.sub(/\A[^_]+/, "utf8")
end
end
@@ -93,15 +97,65 @@ module ActiveRecord
MySQL::SchemaDumper.create(self, options)
end
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, size: limit_to_size(limit, type), unsigned: nil, **)
+ sql =
+ case type.to_s
+ when "integer"
+ integer_to_sql(limit)
+ when "text"
+ type_with_size_to_sql("text", size)
+ when "blob"
+ type_with_size_to_sql("blob", size)
+ when "binary"
+ if (0..0xfff) === limit
+ "varbinary(#{limit})"
+ else
+ type_with_size_to_sql("blob", size)
+ end
+ else
+ super
+ end
+
+ sql = "#{sql} unsigned" if unsigned && type != :primary_key
+ sql
+ end
+
+ def table_alias_length
+ 256 # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
+ end
+
private
CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
+ def row_format_dynamic_by_default?
+ if mariadb?
+ database_version >= "10.2.2"
+ else
+ database_version >= "5.7.9"
+ end
+ end
+
+ def default_row_format
+ return if row_format_dynamic_by_default?
+
+ unless defined?(@default_row_format)
+ if query_value("SELECT @@innodb_file_per_table = 1 AND @@innodb_file_format = 'Barracuda'") == 1
+ @default_row_format = "ROW_FORMAT=DYNAMIC"
+ else
+ @default_row_format = nil
+ end
+ end
+
+ @default_row_format
+ end
+
def schema_creation
MySQL::SchemaCreation.new(self)
end
def create_table_definition(*args)
- MySQL::TableDefinition.new(*args)
+ MySQL::TableDefinition.new(self, *args)
end
def new_column_from_field(table_name, field)
@@ -120,9 +174,8 @@ module ActiveRecord
default,
type_metadata,
field[:Null] == "YES",
- table_name,
default_function,
- field[:Collation],
+ collation: field[:Collation],
comment: field[:Comment].presence
)
end
@@ -171,6 +224,40 @@ module ActiveRecord
schema, name = nil, schema unless name
[schema, name]
end
+
+ def type_with_size_to_sql(type, size)
+ case size&.to_s
+ when nil, "tiny", "medium", "long"
+ "#{size}#{type}"
+ else
+ raise ArgumentError,
+ "#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed."
+ end
+ end
+
+ def limit_to_size(limit, type)
+ case type.to_s
+ when "text", "blob", "binary"
+ case limit
+ when 0..0xff; "tiny"
+ when nil, 0x100..0xffff; nil
+ when 0x10000..0xffffff; "medium"
+ when 0x1000000..0xffffffff; "long"
+ else raise ArgumentError, "No #{type} type has byte size #{limit}"
+ end
+ end
+ end
+
+ def integer_to_sql(limit)
+ case limit
+ when 1; "tinyint"
+ when 2; "smallint"
+ when 3; "mediumint"
+ when nil, 4; "int"
+ when 5..8; "bigint"
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index 7ad0944d51..a7232fa249 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -6,28 +6,33 @@ module ActiveRecord
class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
undef to_yaml if method_defined?(:to_yaml)
+ include Deduplicable
+
attr_reader :extra
- def initialize(type_metadata, extra: "")
+ def initialize(type_metadata, extra: nil)
super(type_metadata)
- @type_metadata = type_metadata
@extra = extra
end
def ==(other)
- other.is_a?(MySQL::TypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ extra == other.extra
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ extra.hash
end
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra]
+ private
+ def deduplicated
+ __setobj__(__getobj__.deduplicate)
+ @extra = -extra if extra
+ super
end
end
end