aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb236
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb125
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb75
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb90
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb89
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb71
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb10
29 files changed, 769 insertions, 414 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index ccd2899489..e389d818fd 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -951,24 +951,5 @@ module ActiveRecord
owner_to_pool && owner_to_pool[owner.name]
end
end
-
- class ConnectionManagement
- def initialize(app)
- @app = app
- end
-
- def call(env)
- testing = env['rack.test']
-
- status, headers, body = @app.call(env)
- proxy = ::Rack::BodyProxy.new(body) do
- ActiveRecord::Base.clear_active_connections! unless testing
- end
- [status, headers, proxy]
- rescue Exception
- ActiveRecord::Base.clear_active_connections! unless testing
- raise
- end
- end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index bb5119d64e..507a925d32 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -30,7 +30,7 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [], preparable: nil)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- if arel.is_a?(String) && preparable.nil?
+ if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
preparable = false
else
preparable = visitor.preparable
@@ -66,8 +66,8 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil, binds = [])
+ exec_query(sql, name, binds).rows
end
- undef_method :select_rows
# Executes the SQL statement in the context of this connection and returns
# the raw result from the connection adapter.
@@ -75,13 +75,14 @@ module ActiveRecord
# method may be manually memory managed. Consider using the exec_query
# wrapper instead.
def execute(sql, name = nil)
+ raise NotImplementedError
end
- undef_method :execute
# Executes +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_query(sql, name = 'SQL', binds = [], prepare: false)
+ raise NotImplementedError
end
# Executes insert +sql+ statement in the context of this connection using
@@ -125,18 +126,21 @@ module ActiveRecord
end
alias create insert
alias insert_sql insert
+ deprecate insert_sql: :insert
# Executes the update statement and returns the number of rows affected.
def update(arel, name = nil, binds = [])
exec_update(to_sql(arel, binds), name, binds)
end
alias update_sql update
+ deprecate update_sql: :update
# Executes the delete statement and returns the number of rows affected.
def delete(arel, name = nil, binds = [])
exec_delete(to_sql(arel, binds), name, binds)
end
alias delete_sql delete
+ deprecate delete_sql: :delete
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
@@ -217,9 +221,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for MySQL versions below 5,
- # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
- # which means the isolation level gets persisted outside the transaction.
+ # isolation level.
def transaction(requires_new: nil, isolation: nil, joinable: true)
if !requires_new && current_transaction.joinable?
if isolation
@@ -289,9 +291,6 @@ module ActiveRecord
exec_rollback_to_savepoint(name)
end
- def exec_rollback_to_savepoint(name = nil) #:nodoc:
- end
-
def default_sequence_name(table, column)
nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 33dbab41cb..0bdfd4f900 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -65,7 +65,7 @@ module ActiveRecord
if @query_cache_enabled && !locked?(arel)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds, preparable: visitor.preparable) }
+ cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 7e3760d34b..860ef17dca 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -82,7 +82,7 @@ module ActiveRecord
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
- column_name
+ column_name.to_s
end
# Quotes the table name. Defaults to column name quoting.
@@ -146,6 +146,10 @@ module ActiveRecord
end
end
+ def quoted_time(value) # :nodoc:
+ quoted_date(value).sub(/\A2000-01-01 /, '')
+ end
+
def prepare_binds_for_database(binds) # :nodoc:
binds.map(&:value_for_database)
end
@@ -166,6 +170,7 @@ module ActiveRecord
# BigDecimals need to be put in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
when Numeric, ActiveSupport::Duration then value.to_s
+ when Type::Time::Value then "'#{quoted_time(value)}'"
when Date, Time then "'#{quoted_date(value)}'"
when Symbol then "'#{quote_string(value.to_s)}'"
when Class then "'#{value}'"
@@ -181,6 +186,7 @@ module ActiveRecord
when false then unquoted_false
# BigDecimals need to be put in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
+ when Type::Time::Value then quoted_time(value)
when Date, Time then quoted_date(value)
when *types_which_need_no_typecasting
value
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
index c0662f8473..67f81e9d10 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -1,10 +1,6 @@
module ActiveRecord
module ConnectionAdapters
- module Savepoints #:nodoc:
- def supports_savepoints?
- true
- end
-
+ module Savepoints
def create_savepoint(name = current_savepoint_name)
execute("SAVEPOINT #{name}")
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 0ba4d94e3c..6add697eeb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -53,8 +53,8 @@ module ActiveRecord
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
end
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
+ create_sql << "(#{statements.join(', ')})" if statements.present?
+ add_table_options!(create_sql, table_options(o))
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
@@ -82,6 +82,19 @@ module ActiveRecord
"DROP CONSTRAINT #{quote_column_name(name)}"
end
+ def table_options(o)
+ table_options = {}
+ table_options[:comment] = o.comment
+ table_options[:options] = o.options
+ table_options
+ end
+
+ def add_table_options!(create_sql, options)
+ if options_sql = options[:options]
+ create_sql << " #{options_sql}"
+ end
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -92,6 +105,7 @@ module ActiveRecord
column_options[:auto_increment] = o.auto_increment
column_options[:primary_key] = o.primary_key
column_options[:collation] = o.collation
+ column_options[:comment] = o.comment
column_options
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index cb10ca9929..bbb0e9249d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -3,14 +3,14 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -207,17 +207,18 @@ module ActiveRecord
include ColumnMethods
attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys
+ attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment
- def initialize(name, temporary, options, as = nil)
+ def initialize(name, temporary = false, options = nil, as = nil, comment: nil)
@columns_hash = {}
@indexes = {}
- @foreign_keys = {}
+ @foreign_keys = []
@primary_keys = nil
@temporary = temporary
@options = options
@as = as
@name = name
+ @comment = comment
end
def primary_keys(name = nil) # :nodoc:
@@ -330,7 +331,10 @@ module ActiveRecord
end
def foreign_key(table_name, options = {}) # :nodoc:
- foreign_keys[table_name] = options
+ table_name_prefix = ActiveRecord::Base.table_name_prefix
+ table_name_suffix = ActiveRecord::Base.table_name_suffix
+ table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}"
+ foreign_keys.push([table_name, options])
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
@@ -373,6 +377,7 @@ module ActiveRecord
column.auto_increment = options[:auto_increment]
column.primary_key = type == :primary_key || options[:primary_key]
column.collation = options[:collation]
+ column.comment = options[:comment]
column
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index b1b6044e72..677a4c6bd0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -7,15 +7,16 @@ module ActiveRecord
# Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column)
- spec = prepare_column_options(column)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
+ spec = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }]
+ spec[:name] = column.name.inspect
+ spec[:type] = schema_type(column).to_s
spec
end
def column_spec_for_primary_key(column)
- return if column.type == :integer
+ return {} if default_primary_key?(column)
spec = { id: schema_type(column).inspect }
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
+ spec.merge!(prepare_column_options(column).except!(:null))
end
# This can be overridden on an Adapter level basis to support other
@@ -23,9 +24,6 @@ module ActiveRecord
# PostgreSQL::ColumnDumper)
def prepare_column_options(column)
spec = {}
- spec[:name] = column.name.inspect
- spec[:type] = schema_type(column).to_s
- spec[:null] = 'false' unless column.null
if limit = schema_limit(column)
spec[:limit] = limit
@@ -42,26 +40,38 @@ module ActiveRecord
default = schema_default(column) if column.has_default?
spec[:default] = default unless default.nil?
+ spec[:null] = 'false' unless column.null
+
if collation = schema_collation(column)
spec[:collation] = collation
end
+ spec[:comment] = column.comment.inspect if column.comment.present?
+
spec
end
# Lists the valid migration options
def migration_keys
- [:name, :limit, :precision, :scale, :default, :null, :collation]
+ [:name, :limit, :precision, :scale, :default, :null, :collation, :comment]
end
private
+ def default_primary_key?(column)
+ schema_type(column) == :integer
+ end
+
def schema_type(column)
- column.type
+ if column.bigint?
+ :bigint
+ else
+ column.type
+ end
end
def schema_limit(column)
- limit = column.limit
+ limit = column.limit unless column.bigint?
limit.inspect if limit && limit != native_database_types[column.type][:limit]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index f0f855963a..99a3e99bdc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -18,6 +18,11 @@ module ActiveRecord
nil
end
+ # Returns the table comment that's stored in database metadata.
+ def table_comment(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
@@ -254,8 +259,8 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+ def create_table(table_name, comment: nil, **options)
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
@@ -283,6 +288,14 @@ module ActiveRecord
end
end
+ if supports_comments? && !supports_comments_in_create?
+ change_table_comment(table_name, comment) if comment
+
+ td.columns.each do |column|
+ change_column_comment(table_name, column.name, column.comment) if column.comment
+ end
+ end
+
result
end
@@ -329,12 +342,13 @@ module ActiveRecord
column_options = options.delete(:column_options) || {}
column_options.reverse_merge!(null: false)
+ type = column_options.delete(:type) || :integer
t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key }
create_table(join_table_name, options.merge!(id: false)) do |td|
- td.integer t1_column, column_options
- td.integer t2_column, column_options
+ td.send type, t1_column, column_options
+ td.send type, t2_column, column_options
yield td if block_given?
end
end
@@ -776,7 +790,8 @@ module ActiveRecord
# [<tt>:type</tt>]
# The reference column type. Defaults to +:integer+.
# [<tt>:index</tt>]
- # Add an appropriate index. Defaults to false.
+ # Add an appropriate index. Defaults to false.
+ # See #add_index for usage of this option.
# [<tt>:foreign_key</tt>]
# Add an appropriate foreign key constraint. Defaults to false.
# [<tt>:polymorphic</tt>]
@@ -796,6 +811,14 @@ module ActiveRecord
#
# add_reference(:products, :supplier, polymorphic: true, index: true)
#
+ # ====== Create a supplier_id column with a unique index
+ #
+ # add_reference(:products, :supplier, index: { unique: true })
+ #
+ # ====== Create a supplier_id column with a named index
+ #
+ # add_reference(:products, :supplier, index: { name: "my_supplier_index" })
+ #
# ====== Create a supplier_id column and appropriate foreign key
#
# add_reference(:products, :supplier, foreign_key: true)
@@ -854,7 +877,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
#
# ====== Creating a foreign key on a specific column
#
@@ -870,7 +893,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
#
# The +options+ hash can include the following keys:
# [<tt>:column</tt>]
@@ -962,11 +985,23 @@ module ActiveRecord
end
def dump_schema_information #:nodoc:
+ versions = ActiveRecord::SchemaMigration.order('version').pluck(:version)
+ insert_versions_sql(versions)
+ end
+
+ def insert_versions_sql(versions) # :nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
- sql = "INSERT INTO #{sm_table} (version) VALUES "
- sql << ActiveRecord::SchemaMigration.order('version').pluck(:version).map {|v| "('#{v}')" }.join(', ')
- sql << ";\n\n"
+ if supports_multi_insert?
+ sql = "INSERT INTO #{sm_table} (version) VALUES "
+ sql << versions.map {|v| "('#{v}')" }.join(', ')
+ sql << ";\n\n"
+ sql
+ else
+ versions.map { |version|
+ "INSERT INTO #{sm_table} (version) VALUES ('#{version}');"
+ }.join "\n\n"
+ end
end
# Should not be called normally, but this operation is non-destructive.
@@ -1003,7 +1038,7 @@ module ActiveRecord
if (duplicate = inserting.detect {|v| inserting.count(v) > 1})
raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
end
- execute "INSERT INTO #{sm_table} (version) VALUES #{inserting.map {|v| "('#{v}')"}.join(', ') }"
+ execute insert_versions_sql(inserting)
end
end
@@ -1051,9 +1086,9 @@ module ActiveRecord
end
# Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
- # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
+ # Additional options (like +:null+) are forwarded to #add_column.
#
- # add_timestamps(:suppliers, null: false)
+ # add_timestamps(:suppliers, null: true)
#
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
@@ -1075,15 +1110,19 @@ module ActiveRecord
Table.new(table_name, base)
end
- def add_index_options(table_name, column_name, options = {}) #:nodoc:
- column_names = Array(column_name)
+ def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
+ if column_name.is_a?(String) && /\W/ === column_name
+ column_names = column_name
+ else
+ column_names = Array(column_name)
+ end
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name)
- index_name ||= index_name(table_name, column: column_names)
+ index_name ||= index_name(table_name, index_name_options(column_names))
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
if options.key?(:algorithm)
@@ -1106,13 +1145,23 @@ module ActiveRecord
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
- [index_name, index_type, index_columns, index_options, algorithm, using]
+ [index_name, index_type, index_columns, index_options, algorithm, using, comment]
end
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+ # Changes the comment for a table or removes it if +nil+.
+ def change_table_comment(table_name, comment)
+ raise NotImplementedError, "#{self.class} does not support changing table comments"
+ end
+
+ # Changes the comment for a column or removes it if +nil+.
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ raise NotImplementedError, "#{self.class} does not support changing column comments"
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -1129,6 +1178,8 @@ module ActiveRecord
# Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
+ return [column_names] if column_names.is_a?(String)
+
option_strings = Hash[column_names.map {|name| [name, '']}]
# add index sort order if supported
@@ -1140,6 +1191,8 @@ module ActiveRecord
end
def index_name_for_remove(table_name, options = {})
+ return options[:name] if can_remove_index_by_name?(options)
+
# if the adapter doesn't support the indexes call the best we can do
# is return the default index name for the options provided
return index_name(table_name, options) unless respond_to?(:indexes)
@@ -1147,7 +1200,7 @@ module ActiveRecord
checks = []
if options.is_a?(Hash)
- checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name)
+ checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name)
column_names = Array(options[:column]).map(&:to_s)
else
column_names = Array(options).map(&:to_s)
@@ -1194,14 +1247,22 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary = false, options = nil, as = nil)
- TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(*args)
+ TableDefinition.new(*args)
end
def create_alter_table(name)
AlterTable.new create_table_definition(name)
end
+ def index_name_options(column_names) # :nodoc:
+ if column_names.is_a?(String)
+ column_names = column_names.scan(/\w+/).join('_')
+ end
+
+ { column: column_names }
+ end
+
def foreign_key_name(table_name, options) # :nodoc:
identifier = "#{table_name}_#{options.fetch(:column)}_fk"
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
@@ -1223,6 +1284,10 @@ module ActiveRecord
default_or_changes
end
end
+
+ def can_remove_index_by_name?(options)
+ options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 6ecdab6eb0..ca795cb1ad 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -188,7 +188,10 @@ module ActiveRecord
transaction = begin_transaction options
yield
rescue Exception => error
- rollback_transaction if transaction
+ if transaction
+ rollback_transaction
+ after_failure_actions(transaction, error)
+ end
raise
ensure
unless error
@@ -214,7 +217,16 @@ module ActiveRecord
end
private
+
NULL_TRANSACTION = NullTransaction.new
+
+ # Deallocate invalidated prepared statements outside of the transaction
+ def after_failure_actions(transaction, error)
+ return unless transaction.is_a?(RealTransaction)
+ return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
+ @connection.clear_cache!
+ end
+
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index d9b42d4283..86de8f1ca0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,5 +1,4 @@
require 'active_record/type'
-require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/determine_if_preparable_visitor'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/sql_type_metadata'
@@ -28,7 +27,6 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
autoload :ConnectionHandler
- autoload :ConnectionManagement
end
autoload_under 'abstract' do
@@ -69,6 +67,7 @@ module ActiveRecord
include QueryCache
include ActiveSupport::Callbacks
include ColumnDumper
+ include Savepoints
SIMPLE_INT = /\A\d+\z/
@@ -106,8 +105,15 @@ module ActiveRecord
@config = config
@pool = nil
@schema_cache = SchemaCache.new self
- @visitor = nil
- @prepared_statements = false
+ @quoted_column_names, @quoted_table_names = {}, {}
+ @visitor = arel_visitor
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
+ else
+ @prepared_statements = false
+ end
end
class Version
@@ -143,6 +149,10 @@ module ActiveRecord
end
end
+ def arel_visitor # :nodoc:
+ Arel::Visitors::ToSql.new(self)
+ end
+
def valid_type?(type)
true
end
@@ -238,6 +248,11 @@ module ActiveRecord
false
end
+ # Does this adapter support expression indices?
+ def supports_expression_index?
+ false
+ end
+
# Does this adapter support explain?
def supports_explain?
false
@@ -279,6 +294,21 @@ module ActiveRecord
false
end
+ # Does this adapter support metadata comments on database objects (tables, columns, indexes)?
+ def supports_comments?
+ false
+ end
+
+ # Can comments for tables, columns, and indexes be specified in create/alter table statements?
+ def supports_comments_in_create?
+ false
+ end
+
+ # Does this adapter support multi-value insert?
+ def supports_multi_insert?
+ true
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -380,12 +410,6 @@ module ActiveRecord
@connection
end
- def create_savepoint(name = nil)
- end
-
- def release_savepoint(name = nil)
- end
-
def case_sensitive_comparison(table, attribute, column, value)
if value.nil?
table[attribute].eq(value)
@@ -398,7 +422,7 @@ module ActiveRecord
if can_perform_case_insensitive_comparison_for?(column)
table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new))
else
- case_sensitive_comparison(table, attribute, column, value)
+ table[attribute].eq(Arel::Nodes::BindParam.new)
end
end
@@ -422,8 +446,8 @@ module ActiveRecord
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
- Column.new(name, default, sql_type_metadata, null, default_function, collation)
+ def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc:
+ Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation)
end
def lookup_cast_type(sql_type) # :nodoc:
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..d5fe880841 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,6 +1,8 @@
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'
@@ -11,8 +13,8 @@ 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)
@@ -22,6 +24,10 @@ module ActiveRecord
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,8 +38,6 @@ 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 },
@@ -52,18 +56,16 @@ 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
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
+ @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."
@@ -78,10 +80,14 @@ module ActiveRecord
}
end
- def version
+ def version #:nodoc:
@version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
end
+ def mariadb? # :nodoc:
+ full_version =~ /mariadb/i
+ 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?
@@ -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 ======================================
#++
@@ -231,11 +213,6 @@ module ActiveRecord
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) }
@@ -301,9 +278,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,7 +289,7 @@ 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
@@ -370,8 +347,7 @@ 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)}"
@@ -386,8 +362,7 @@ module ActiveRecord
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)}"
@@ -408,7 +383,7 @@ 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)
+ 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]
@@ -421,21 +396,28 @@ 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:
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT table_comment
+ FROM information_schema.tables
+ WHERE table_name=#{quote(table_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:
@@ -518,11 +500,17 @@ 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}"
+ sql << " COMMENT #{quote(comment)}" if comment
+ execute sql
end
def foreign_keys(table_name)
+ raise ArgumentError unless table_name.present?
+
+ schema, name = extract_schema_qualified_name(table_name)
+
fk_info = select_all <<-SQL.strip_heredoc
SELECT fk.referenced_table_name as 'to_table'
,fk.referenced_column_name as 'primary_key'
@@ -530,8 +518,8 @@ module ActiveRecord
,fk.constraint_name as 'name'
FROM information_schema.key_column_usage fk
WHERE fk.referenced_column_name is not null
- AND fk.table_schema = '#{@config[:database]}'
- AND fk.table_name = '#{table_name}'
+ AND fk.table_schema = #{quote(schema)}
+ AND fk.table_name = #{quote(name)}
SQL
create_table_info = create_table_info(table_name)
@@ -557,7 +545,12 @@ module ActiveRecord
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')
+
+ # strip COMMENT
+ raw_table_options.sub!(/ COMMENT='.+'/, '')
+
+ raw_table_options
end
# Maps logical Rails types to MySQL-specific data types.
@@ -585,8 +578,7 @@ module ActiveRecord
# 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,8 +586,7 @@ 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 column_name
@@ -608,20 +599,17 @@ module ActiveRecord
end
def case_sensitive_comparison(table, attribute, column, value)
- if value.nil? || column.case_sensitive?
- super
- else
+ if !value.nil? && 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
@@ -671,7 +659,7 @@ module ActiveRecord
register_integer_type m, %r(^smallint)i, limit: 2
register_integer_type m, %r(^tinyint)i, limit: 1
- m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.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'
@@ -741,6 +729,8 @@ module ActiveRecord
RecordNotUnique.new(message)
when 1452
InvalidForeignKey.new(message)
+ when 1406
+ ValueTooLong.new(message)
else
super
end
@@ -826,10 +816,6 @@ module ActiveRecord
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
@@ -850,9 +836,19 @@ module ActiveRecord
# 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' : ''
+ 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
@@ -874,7 +870,13 @@ module ActiveRecord
end.compact.join(', ')
# ...and send them all in one query
- @connection.query "SET #{encoding} #{variable_assignments}"
+ @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(structure, name, action) # :nodoc:
@@ -894,8 +896,14 @@ module ActiveRecord
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)
+ 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 integer_to_sql(limit) # :nodoc:
@@ -940,8 +948,8 @@ module ActiveRecord
class MysqlString < Type::String # :nodoc:
def serialize(value)
case value
- when true then "1"
- when false then "0"
+ when true then MySQL::Quoting::QUOTED_TRUE
+ when false then MySQL::Quoting::QUOTED_FALSE
else super
end
end
@@ -950,8 +958,8 @@ module ActiveRecord
def cast_value(value)
case value
- when true then "1"
- when false then "0"
+ when true then MySQL::Quoting::QUOTED_TRUE
+ when false then MySQL::Quoting::QUOTED_FALSE
else super
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 10f908538f..28f0c8686a 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -1,11 +1,9 @@
-require 'set'
-
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
+ attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,14 +13,15 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil)
@name = name.freeze
+ @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@default_function = default_function
@collation = collation
- @table_name = nil
+ @comment = comment
end
def has_default?
@@ -54,7 +53,7 @@ module ActiveRecord
protected
def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, default_function, collation]
+ [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
new file mode 100644
index 0000000000..13c9b6cbd9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -0,0 +1,125 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module DatabaseStatements
+ # Returns an ActiveRecord::Result instance.
+ def select_all(arel, name = nil, binds = [], preparable: nil)
+ result = if ExplainRegistry.collect? && prepared_statements
+ unprepared_statement { super }
+ else
+ super
+ end
+ @connection.next_result while @connection.more_results?
+ result
+ end
+
+ # Returns a record hash with the column names as keys and column values
+ # as values.
+ def select_one(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation(arel, binds)
+ @connection.query_options.merge!(as: :hash)
+ select_result(to_sql(arel, binds), name, binds) do |result|
+ @connection.next_result while @connection.more_results?
+ result.first
+ end
+ ensure
+ @connection.query_options.merge!(as: :array)
+ end
+
+ # Returns an array of arrays containing the field values.
+ # Order is the same as that returned by +columns+.
+ def select_rows(sql, name = nil, binds = [])
+ select_result(sql, name, binds) do |result|
+ @connection.next_result while @connection.more_results?
+ result.to_a
+ end
+ end
+
+ # Executes the SQL statement in the context of this connection.
+ def execute(sql, name = nil)
+ if @connection
+ # 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
+ end
+
+ super
+ end
+
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) do |result|
+ ActiveRecord::Result.new(result.fields, result.to_a) if result
+ end
+ else
+ exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
+ ActiveRecord::Result.new(result.fields, result.to_a) if result
+ end
+ end
+ end
+
+ def exec_delete(sql, name, binds)
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) { @connection.affected_rows }
+ else
+ exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
+ end
+ end
+ alias :exec_update :exec_delete
+
+ protected
+
+ def last_inserted_id(result)
+ @connection.last_id
+ end
+
+ private
+
+ def select_result(sql, name = nil, binds = [])
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) { |result| yield result }
+ else
+ exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result }
+ end
+ end
+
+ def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
+ if @connection
+ # 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
+ end
+
+ type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
+
+ log(sql, name, binds) do
+ if cache_stmt
+ cache = @statements[sql] ||= {
+ stmt: @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ else
+ stmt = @connection.prepare(sql)
+ end
+
+ begin
+ result = stmt.execute(*type_casted_binds)
+ rescue Mysql2::Error => e
+ if cache_stmt
+ @statements.delete(sql)
+ else
+ stmt.close
+ end
+ raise e
+ end
+
+ ret = yield stmt, result
+ result.free if result
+ stmt.close unless cache_stmt
+ ret
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
new file mode 100644
index 0000000000..fbab654112
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -0,0 +1,51 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module Quoting # :nodoc:
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
+
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ end
+
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= super.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
+
+ private
+
+ def _quote(value)
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ 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 1e2c859af9..0384079da2 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -2,6 +2,9 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation
+ delegate :quote, to: :@conn
+ private :quote
+
private
def visit_DropForeignKey(name)
@@ -22,6 +25,14 @@ module ActiveRecord
add_column_position!(change_column_sql, column_options(o.column))
end
+ def add_table_options!(create_sql, options)
+ super
+
+ if comment = options[:comment]
+ create_sql << " COMMENT #{quote(comment)}"
+ end
+ end
+
def column_options(o)
column_options = super
column_options[:charset] = o.charset
@@ -29,13 +40,21 @@ module ActiveRecord
end
def add_column_options!(sql, options)
- if options[:charset]
- sql << " CHARACTER SET #{options[:charset]}"
+ if charset = options[:charset]
+ sql << " CHARACTER SET #{charset}"
end
- if options[:collation]
- sql << " COLLATE #{options[:collation]}"
+
+ if collation = options[:collation]
+ sql << " COLLATE #{collation}"
end
+
super
+
+ if comment = options[:comment]
+ sql << " COMMENT #{quote(comment)}"
+ end
+
+ sql
end
def add_column_position!(sql, options)
@@ -44,12 +63,14 @@ module ActiveRecord
elsif options[:after]
sql << " AFTER #{quote_column_name(options[:after])}"
end
+
sql
end
def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
+ index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options)
+ index_option = " COMMENT #{quote(comment)}" if comment
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_option} "
end
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 ccf5b6cadc..2ba9657f24 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -3,18 +3,13 @@ module ActiveRecord
module MySQL
module ColumnDumper
def column_spec_for_primary_key(column)
- spec = {}
if column.bigint?
- spec[:id] = ':bigint'
+ spec = { id: :bigint.inspect }
spec[:default] = schema_default(column) || 'nil' unless column.auto_increment?
- spec[:unsigned] = 'true' if column.unsigned?
- elsif column.auto_increment?
- spec[:unsigned] = 'true' if column.unsigned?
- return if spec.empty?
else
- spec[:id] = schema_type(column).inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ spec = super
end
+ spec[:unsigned] = 'true' if column.unsigned?
spec
end
@@ -30,6 +25,10 @@ module ActiveRecord
private
+ def default_primary_key?(column)
+ super && column.auto_increment?
+ end
+
def schema_type(column)
if column.sql_type == 'tinyblob'
:blob
@@ -38,16 +37,12 @@ module ActiveRecord
end
end
- def schema_limit(column)
- super unless column.type == :boolean
- end
-
def schema_precision(column)
super unless /time/ === column.sql_type && column.precision == 0
end
def schema_collation(column)
- if column.collation && table_name = column.instance_variable_get(:@table_name)
+ if column.collation && table_name = column.table_name
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
column.collation.inspect if column.collation != @table_collation_cache[table_name]
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 23b33a3555..22d35f1db5 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,7 +1,9 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
+require 'active_record/connection_adapters/mysql/database_statements'
gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
+raise 'mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+' if Mysql2::VERSION == '0.4.3'
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -16,7 +18,7 @@ module ActiveRecord
if config[:flags].kind_of? Array
config[:flags].push "FOUND_ROWS".freeze
else
- config[:flags] |= Mysql2::Client::FOUND_ROWS
+ config[:flags] |= Mysql2::Client::FOUND_ROWS
end
end
@@ -35,9 +37,11 @@ module ActiveRecord
class Mysql2Adapter < AbstractMysqlAdapter
ADAPTER_NAME = 'Mysql2'.freeze
+ include MySQL::DatabaseStatements
+
def initialize(connection, logger, connection_options, config)
super
- @prepared_statements = false
+ @prepared_statements = false unless config.key?(:prepared_statements)
configure_connection
end
@@ -45,6 +49,18 @@ module ActiveRecord
!mariadb? && version >= '5.7.8'
end
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ true
+ end
+
+ def supports_savepoints?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -95,61 +111,6 @@ module ActiveRecord
end
end
- #--
- # DATABASE STATEMENTS ======================================
- #++
-
- # Returns a record hash with the column names as keys and column values
- # as values.
- def select_one(arel, name = nil, binds = [])
- arel, binds = binds_from_relation(arel, binds)
- execute(to_sql(arel, binds), name).each(as: :hash) do |row|
- @connection.next_result while @connection.more_results?
- return row
- end
- end
-
- # Returns an array of arrays containing the field values.
- # Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil, binds = [])
- result = execute(sql, name)
- @connection.next_result while @connection.more_results?
- result.to_a
- end
-
- # Executes the SQL statement in the context of this connection.
- def execute(sql, name = nil)
- if @connection
- # 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
- end
-
- super
- end
-
- def exec_query(sql, name = 'SQL', binds = [], prepare: false)
- result = execute(sql, name)
- @connection.next_result while @connection.more_results?
- ActiveRecord::Result.new(result.fields, result.to_a)
- end
-
- alias exec_without_stmt exec_query
-
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
- execute to_sql(sql, binds), name
- end
-
- def exec_delete(sql, name, binds)
- execute to_sql(sql, binds), name
- @connection.affected_rows
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_id
- end
-
private
def connect
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index bfa03fa136..3ad1911a28 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -8,7 +8,6 @@ module ActiveRecord
def serial?
return unless default_function
- table_name = @table_name || '(?<table_name>.+)'
%r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 6aa264d766..6f2e03b370 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -118,7 +118,7 @@ module ActiveRecord
alias :exec_update :exec_delete
def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc:
- unless pk
+ if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
pk = primary_key(table_ref) if table_ref
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index eeccb09bdf..838cb63281 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -1,3 +1,5 @@
+require 'ipaddr'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index c1c77a967e..6414459cd1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -27,8 +27,8 @@ module ActiveRecord
# - schema_name."table.name"
# - "schema.name".table_name
# - "schema.name"."table.name"
- def quote_table_name(name)
- Utils.extract_schema_qualified_name(name.to_s).quoted
+ def quote_table_name(name) # :nodoc:
+ @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted
end
# Quotes schema names for use in SQL queries.
@@ -41,8 +41,8 @@ module ActiveRecord
end
# Quotes column names for use in SQL queries.
- def quote_column_name(name) #:nodoc:
- PGconn.quote_ident(name.to_s)
+ def quote_column_name(name) # :nodoc:
+ @quoted_column_names[name] ||= PGconn.quote_ident(super)
end
# Quote date/time values for use in SQL input.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
index b82bdb8b0c..a1e10fd364 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -3,16 +3,9 @@ module ActiveRecord
module PostgreSQL
module ColumnDumper
def column_spec_for_primary_key(column)
- spec = {}
- if column.serial?
- return unless column.bigint?
- spec[:id] = ':bigserial'
- elsif column.type == :uuid
- spec[:id] = ':uuid'
- spec[:default] = schema_default(column) || 'nil'
- else
- spec[:id] = schema_type(column).inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ spec = super
+ if schema_type(column) == :uuid
+ spec[:default] ||= 'nil'
end
spec
end
@@ -31,6 +24,10 @@ module ActiveRecord
private
+ def default_primary_key?(column)
+ schema_type(column) == :serial
+ end
+
def schema_type(column)
return super unless column.serial?
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 67e727d8ed..074ebb90a5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/strip'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -172,7 +174,11 @@ module ActiveRecord
table = Utils.extract_schema_qualified_name(table_name.to_s)
result = query(<<-SQL, 'SCHEMA')
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment,
+ (SELECT COUNT(*) FROM pg_opclass o
+ JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c
+ ON o.oid = c.oid WHERE o.opcdefault = 'f')
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -190,43 +196,61 @@ module ActiveRecord
indkey = row[2].split(" ").map(&:to_i)
inddef = row[3]
oid = row[4]
+ comment = row[5]
+ opclass = row[6]
- columns = Hash[query(<<-SQL, "SCHEMA")]
- SELECT a.attnum, a.attname
- FROM pg_attribute a
- WHERE a.attrelid = #{oid}
- AND a.attnum IN (#{indkey.join(",")})
- SQL
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten
- column_names = columns.values_at(*indkey).compact
+ if indkey.include?(0) || opclass > 0
+ columns = expressions
+ else
+ columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
+ SELECT a.attnum, a.attname
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
- unless column_names.empty?
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
-
- IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ orders = Hash[
+ expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
+ ]
end
+
+ IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment)
end.compact
end
# Returns the list of all column definitions for a table.
- def columns(table_name)
- # Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
+ def columns(table_name) # :nodoc:
+ table_name = table_name.to_s
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment|
oid = oid.to_i
fmod = fmod.to_i
type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)
+ new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment.presence)
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)
+ def new_column(*args) # :nodoc:
+ PostgreSQLColumn.new(*args)
+ end
+
+ # Returns a comment stored in database for given table
+ def table_comment(table_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(table_name.to_s)
+ if name.identifier
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relname = #{quote(name.identifier)}
+ AND c.relkind IN ('r') -- (r)elation/table
+ AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'}
+ SQL
+ end
end
# Returns the current database name.
@@ -325,7 +349,7 @@ module ActiveRecord
select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA')
else
- @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
end
end
end
@@ -340,7 +364,7 @@ module ActiveRecord
end
if @logger && pk && !sequence
- @logger.warn "#{table} has primary key #{pk} with no default sequence"
+ @logger.warn "#{table} has primary key #{pk} with no default sequence."
end
if pk && sequence
@@ -445,6 +469,7 @@ module ActiveRecord
def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
@@ -466,6 +491,7 @@ module ActiveRecord
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
# Changes the default value of a table column.
@@ -494,6 +520,18 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
+ def change_column_comment(table_name, column_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
+ end
+
+ # Adds comment for given table or drops it if +comment+ is a +nil+
+ def change_table_comment(table_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
+ end
+
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name) #:nodoc:
clear_cache!
@@ -502,8 +540,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do
+ execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
+ end
end
def remove_index(table_name, options = {}) #:nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index beaeef3c78..39a2cbbda7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -16,8 +16,6 @@ require "active_record/connection_adapters/postgresql/type_metadata"
require "active_record/connection_adapters/postgresql/utils"
require "active_record/connection_adapters/statement_pool"
-require 'ipaddr'
-
module ActiveRecord
module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects
@@ -119,12 +117,15 @@ module ActiveRecord
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
include PostgreSQL::ColumnDumper
- include Savepoints
def schema_creation # :nodoc:
PostgreSQL::SchemaCreation.new self
end
+ def arel_visitor # :nodoc:
+ Arel::Visitors::PostgreSQL.new(self)
+ end
+
# Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
@@ -139,6 +140,10 @@ module ActiveRecord
true
end
+ def supports_expression_index?
+ true
+ end
+
def supports_transaction_isolation?
true
end
@@ -159,6 +164,18 @@ module ActiveRecord
postgresql_version >= 90200
end
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ false
+ end
+
+ def supports_savepoints?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -195,14 +212,6 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger, config)
- @visitor = Arel::Visitors::PostgreSQL.new self
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
-
@connection_parameters = connection_parameters
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@@ -212,7 +221,7 @@ module ActiveRecord
connect
add_pg_encoders
@statements = StatementPool.new @connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config[:statement_limit])
if postgresql_version < 90100
raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
@@ -398,6 +407,7 @@ module ActiveRecord
protected
# See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
+ VALUE_LIMIT_VIOLATION = "22001"
FOREIGN_KEY_VIOLATION = "23503"
UNIQUE_VIOLATION = "23505"
@@ -409,6 +419,8 @@ module ActiveRecord
RecordNotUnique.new(message)
when FOREIGN_KEY_VIOLATION
InvalidForeignKey.new(message)
+ when VALUE_LIMIT_VIOLATION
+ ValueTooLong.new(message)
else
super
end
@@ -598,25 +610,41 @@ module ActiveRecord
@connection.exec_prepared(stmt_key, type_casted_binds)
end
rescue ActiveRecord::StatementInvalid => e
- pgerror = e.cause
+ raise unless is_cached_plan_failure?(e)
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
+ # Nothing we can do if we are in a transaction because all commands
+ # will raise InFailedSQLTransaction
+ if in_transaction?
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
+ else
+ # outside of transactions we can simply flush this query and retry
@statements.delete sql_key(sql)
retry
- else
- raise e
end
end
+ # Annoyingly, the code for prepared statements whose return value may
+ # have changed is FEATURE_NOT_SUPPORTED.
+ #
+ # This covers various different error types so we need to do additional
+ # work to classify the exception definitively as a
+ # ActiveRecord::PreparedStatementCacheExpired
+ #
+ # Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'.freeze
+ def is_cached_plan_failure?(e)
+ pgerror = e.cause
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
+ rescue
+ false
+ end
+
+ def in_transaction?
+ open_transactions > 0
+ end
+
# Returns the statement identifier for the client side cache
# of statements
def sql_key(sql)
@@ -696,7 +724,7 @@ module ActiveRecord
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
- # SELECT column.name, column.type, default.value
+ # SELECT column.name, column.type, default.value, column.comment
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
@@ -716,7 +744,8 @@ module ActiveRecord
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
(SELECT c.collname FROM pg_collation c, pg_type t
- WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
+ col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
@@ -730,8 +759,8 @@ module ActiveRecord
$1.strip if $1
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- PostgreSQL::TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(*args) # :nodoc:
+ PostgreSQL::TableDefinition.new(*args)
end
def can_perform_case_insensitive_comparison_for?(column)
@@ -799,7 +828,7 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
- ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql)
+ ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
new file mode 100644
index 0000000000..d5a181d3e2
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module Quoting # :nodoc:
+ def quote_string(s)
+ @connection.class.quote(s)
+ end
+
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ end
+
+ def quoted_time(value)
+ quoted_date(value)
+ end
+
+ private
+
+ def _quote(value)
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when BigDecimal
+ value.to_f
+ when String
+ if value.encoding == Encoding::ASCII_8BIT
+ super(value.encode(Encoding::UTF_8))
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
index fe1dcbd710..70c0d28830 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -3,6 +3,13 @@ module ActiveRecord
module SQLite3
class SchemaCreation < AbstractAdapter::SchemaCreation
private
+
+ def column_options(o)
+ options = super
+ options[:null] = false if o.primary_key
+ options
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index c65d33ccb3..1d7ce92b29 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,6 +1,7 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_record/connection_adapters/sqlite3/explain_pretty_printer'
+require 'active_record/connection_adapters/sqlite3/quoting'
require 'active_record/connection_adapters/sqlite3/schema_creation'
gem 'sqlite3', '~> 1.3.6'
@@ -49,7 +50,8 @@ module ActiveRecord
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
ADAPTER_NAME = 'SQLite'.freeze
- include Savepoints
+
+ include SQLite3::Quoting
NATIVE_DATABASE_TYPES = {
primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
@@ -77,21 +79,15 @@ module ActiveRecord
SQLite3::SchemaCreation.new self
end
+ def arel_visitor # :nodoc:
+ Arel::Visitors::SQLite.new(self)
+ end
+
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
@active = nil
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
-
- @visitor = Arel::Visitors::SQLite.new self
- @quoted_column_names = {}
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
end
def supports_ddl_transactions?
@@ -133,6 +129,10 @@ module ActiveRecord
true
end
+ def supports_multi_insert?
+ sqlite_version >= '3.7.11'
+ end
+
def active?
@active != false
end
@@ -174,44 +174,6 @@ module ActiveRecord
true
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- case value
- when Type::Binary::Data
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def _type_cast(value) # :nodoc:
- case value
- when BigDecimal
- value.to_f
- when String
- if value.encoding == Encoding::ASCII_8BIT
- super(value.encode(Encoding::UTF_8))
- else
- super
- end
- else
- super
- end
- end
-
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
-
- def quote_table_name_for_assignment(table, attr)
- quote_column_name(attr)
- end
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
- end
-
#--
# DATABASE STATEMENTS ======================================
#++
@@ -266,10 +228,6 @@ module ActiveRecord
log(sql, name) { @connection.execute(sql) }
end
- def select_rows(sql, name = nil, binds = [])
- exec_query(sql, name, binds).rows
- end
-
def begin_db_transaction #:nodoc:
log('begin transaction',nil) { @connection.transaction }
end
@@ -337,7 +295,8 @@ module ActiveRecord
end
# Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) #:nodoc:
+ def columns(table_name) # :nodoc:
+ table_name = table_name.to_s
table_structure(table_name).map do |field|
case field["dflt_value"]
when /^null$/i
@@ -351,7 +310,7 @@ module ActiveRecord
collation = field['collation']
sql_type = field['type']
type_metadata = fetch_type_metadata(sql_type)
- new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
+ new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, table_name, nil, collation)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 57463dd749..9b0ed3e08b 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -1,11 +1,13 @@
module ActiveRecord
module ConnectionAdapters
- class StatementPool
+ class StatementPool # :nodoc:
include Enumerable
- def initialize(max = 1000)
+ DEFAULT_STATEMENT_LIMIT = 1000
+
+ def initialize(statement_limit = nil)
@cache = Hash.new { |h,pid| h[pid] = {} }
- @max = max
+ @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT
end
def each(&block)
@@ -25,7 +27,7 @@ module ActiveRecord
end
def []=(sql, stmt)
- while @max <= cache.size
+ while @statement_limit <= cache.size
dealloc(cache.shift.last)
end
cache[sql] = stmt