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.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb63
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb56
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb62
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/deduplicable.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb9
-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_dumper.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb55
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb1
43 files changed, 580 insertions, 166 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 68498b5dc5..36001efdd5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -3,6 +3,7 @@
require "thread"
require "concurrent/map"
require "monitor"
+require "weakref"
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -19,6 +20,26 @@ module ActiveRecord
end
module ConnectionAdapters
+ module AbstractPool # :nodoc:
+ def get_schema_cache(connection)
+ @schema_cache ||= SchemaCache.new(connection)
+ @schema_cache.connection = connection
+ @schema_cache
+ end
+
+ def set_schema_cache(cache)
+ @schema_cache = cache
+ end
+ end
+
+ class NullPool # :nodoc:
+ include ConnectionAdapters::AbstractPool
+
+ def initialize
+ @schema_cache = nil
+ end
+ end
+
# Connection pool base class for managing Active Record database
# connections.
#
@@ -146,7 +167,6 @@ module ActiveRecord
end
private
-
def internal_poll(timeout)
no_wait_poll || (timeout && wait_poll(timeout))
end
@@ -294,23 +314,50 @@ module ActiveRecord
@frequency = frequency
end
+ @mutex = Mutex.new
+ @pools = {}
+
+ class << self
+ def register_pool(pool, frequency) # :nodoc:
+ @mutex.synchronize do
+ unless @pools.key?(frequency)
+ @pools[frequency] = []
+ spawn_thread(frequency)
+ end
+ @pools[frequency] << WeakRef.new(pool)
+ end
+ end
+
+ private
+ def spawn_thread(frequency)
+ Thread.new(frequency) do |t|
+ loop do
+ sleep t
+ @mutex.synchronize do
+ @pools[frequency].select!(&:weakref_alive?)
+ @pools[frequency].each do |p|
+ p.reap
+ p.flush
+ rescue WeakRef::RefError
+ end
+ end
+ end
+ end
+ end
+ end
+
def run
return unless frequency && frequency > 0
- Thread.new(frequency, pool) { |t, p|
- loop do
- sleep t
- p.reap
- p.flush
- end
- }
+ self.class.register_pool(pool, frequency)
end
end
include MonitorMixin
include QueryCache::ConnectionPoolConfiguration
+ include ConnectionAdapters::AbstractPool
attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache
- attr_reader :spec, :connections, :size, :reaper
+ attr_reader :spec, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -379,7 +426,7 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a cache keyed by a thread.
def connection
- @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
+ @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
end
# Returns true if there is an open connection being used for the current thread.
@@ -388,7 +435,7 @@ module ActiveRecord
# #connection or #with_connection methods. Connections obtained through
# #checkout will not be detected by #active_connection?
def active_connection?
- @thread_cached_conns[connection_cache_key(Thread.current)]
+ @thread_cached_conns[connection_cache_key(current_thread)]
end
# Signal that the thread is finished with the current connection.
@@ -423,6 +470,21 @@ module ActiveRecord
synchronize { @connections.any? }
end
+ # Returns an array containing the connections currently in the pool.
+ # Access to the array does not require synchronization on the pool because
+ # the array is newly created and not retained by the pool.
+ #
+ # However; this method bypasses the ConnectionPool's thread-safe connection
+ # access pattern. A returned connection may be owned by another thread,
+ # unowned, or by happen-stance owned by the calling thread.
+ #
+ # Calling methods on a connection without ownership is subject to the
+ # thread-safety guarantees of the underlying method. Many of the methods
+ # on connection adapter classes are inherently multi-thread unsafe.
+ def connections
+ synchronize { @connections.dup }
+ end
+
# Disconnects all connections in the pool, and clears the pool.
#
# Raises:
@@ -668,6 +730,10 @@ module ActiveRecord
thread
end
+ def current_thread
+ @lock_thread || Thread.current
+ end
+
# Take control of all existing connections so a "group" action such as
# reload/disconnect can be performed safely. It is no longer enough to
# wrap it in +synchronize+ because some pool's actions are allowed
@@ -809,7 +875,6 @@ module ActiveRecord
def new_connection
Base.send(spec.adapter_method, spec.config).tap do |conn|
- conn.schema_cache = schema_cache.dup if schema_cache
conn.check_version
end
end
@@ -938,15 +1003,30 @@ module ActiveRecord
end
end
+ attr_reader :prevent_writes
+
def initialize
# These caches are keyed by spec.name (ConnectionSpecification#name).
@owner_to_pool = ConnectionHandler.create_owner_to_pool
+ @prevent_writes = false
# Backup finalizer: if the forked child never needed a pool, the above
# early discard has not occurred
ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
end
+ # Prevent writing to the database regardless of role.
+ #
+ # In some cases you may want to prevent writes to the database
+ # even if you are on a database that can write. `while_preventing_writes`
+ # will prevent writes to the database for the duration of the block.
+ def while_preventing_writes
+ original, @prevent_writes = @prevent_writes, true
+ yield
+ ensure
+ @prevent_writes = original
+ end
+
def connection_pool_list
owner_to_pool.values.compact
end
@@ -1064,7 +1144,6 @@ module ActiveRecord
end
private
-
def owner_to_pool
@owner_to_pool[Process.pid]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 75e959045e..d932f068f2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
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 a7753e3e9c..768122b4d2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -33,17 +33,17 @@ module ActiveRecord
end
def enable_query_cache!
- @query_cache_enabled[connection_cache_key(Thread.current)] = true
+ @query_cache_enabled[connection_cache_key(current_thread)] = true
connection.enable_query_cache! if active_connection?
end
def disable_query_cache!
- @query_cache_enabled.delete connection_cache_key(Thread.current)
+ @query_cache_enabled.delete connection_cache_key(current_thread)
connection.disable_query_cache! if active_connection?
end
def query_cache_enabled
- @query_cache_enabled[connection_cache_key(Thread.current)]
+ @query_cache_enabled[connection_cache_key(current_thread)]
end
end
@@ -109,7 +109,6 @@ module ActiveRecord
end
private
-
def cache_sql(sql, name, binds)
@lock.synchronize do
result =
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 2877530917..93273f6cf6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -114,16 +114,16 @@ module ActiveRecord
# if the value is a Time responding to usec.
def quoted_date(value)
if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
-
- if value.respond_to?(zone_conversion_method)
- value = value.send(zone_conversion_method)
+ if ActiveRecord::Base.default_timezone == :utc
+ value = value.getutc if value.respond_to?(:getutc) && !value.utc?
+ else
+ value = value.getlocal if value.respond_to?(:getlocal)
end
end
result = value.to_s(:db)
if value.respond_to?(:usec) && value.usec > 0
- "#{result}.#{sprintf("%06d", value.usec)}"
+ result << "." << sprintf("%06d", value.usec)
else
result
end
@@ -142,6 +142,59 @@ module ActiveRecord
value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
end
+ def column_name_matcher # :nodoc:
+ COLUMN_NAME
+ end
+
+ def column_name_with_order_matcher # :nodoc:
+ COLUMN_NAME_WITH_ORDER
+ end
+
+ # Regexp for column names (with or without a table name prefix).
+ # Matches the following:
+ #
+ # "#{table_name}.#{column_name}"
+ # "#{column_name}"
+ COLUMN_NAME = /
+ \A
+ (
+ (?:
+ # table_name.column_name | function(one or no argument)
+ ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\)
+ )
+ (?:(?:\s+AS)?\s+\w+)?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ # Regexp for column names with order (with or without a table name prefix,
+ # with or without various order modifiers). Matches the following:
+ #
+ # "#{table_name}.#{column_name}"
+ # "#{table_name}.#{column_name} #{direction}"
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
+ # "#{table_name}.#{column_name} NULLS LAST"
+ # "#{column_name}"
+ # "#{column_name} #{direction}"
+ # "#{column_name} #{direction} NULLS FIRST"
+ # "#{column_name} NULLS LAST"
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # table_name.column_name | function(one or no argument)
+ ((?:\w+\.)?\w+) | \w+\((?:|\g<2>)\)
+ )
+ (?:\s+ASC|\s+DESC)?
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
private
def type_casted_binds(binds)
if binds.first.is_a?(Array)
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 7d20825a75..23c993cfc3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -19,7 +19,6 @@ module ActiveRecord
to: :@conn, private: true
private
-
def visit_AlterTable(o)
sql = +"ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| accept col }.join(" ")
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 688eea75e8..dbd533b4b3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -264,8 +264,7 @@ module ActiveRecord
if_not_exists: false,
options: nil,
as: nil,
- comment: nil,
- **
+ comment: nil
)
@conn = conn
@columns_hash = {}
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 622e00fffb..fb56e712be 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -15,7 +15,7 @@ module ActiveRecord
def column_spec_for_primary_key(column)
return {} if default_primary_key?(column)
spec = { id: schema_type(column).inspect }
- spec.merge!(prepare_column_options(column).except!(:null))
+ spec.merge!(prepare_column_options(column).except!(:null, :comment))
spec[:default] ||= "nil" if explicit_primary_key_default?(column)
spec
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 f97842b3f5..88367c79a1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -2,7 +2,6 @@
require "active_record/migration/join_table"
require "active_support/core_ext/string/access"
-require "active_support/deprecation"
require "digest/sha2"
module ActiveRecord
@@ -101,7 +100,7 @@ module ActiveRecord
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name).map(&:to_s)
checks = []
- checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| Array(i.columns) == column_names }
checks << lambda { |i| i.unique } if options[:unique]
checks << lambda { |i| i.name == options[:name].to_s } if options[:name]
@@ -291,25 +290,27 @@ 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)
+ def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
+ td = create_table_definition(
+ table_name, options.extract!(:temporary, :if_not_exists, :options, :as, :comment)
+ )
- if options[:id] != false && !options[:as]
- pk = options.fetch(:primary_key) do
- Base.get_primary_key table_name.to_s.singularize
- end
+ if id && !td.as
+ pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)
if pk.is_a?(Array)
td.primary_keys pk
else
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ td.primary_key pk, id, options
end
end
yield td if block_given?
- if options[:force]
- drop_table(table_name, options.merge(if_exists: true))
+ if force
+ drop_table(table_name, force: force, if_exists: true)
+ else
+ schema_cache.clear_data_source_cache!(table_name.to_s)
end
result = execute schema_creation.accept td
@@ -321,7 +322,7 @@ module ActiveRecord
end
if supports_comments? && !supports_comments_in_create?
- if table_comment = options[:comment].presence
+ if table_comment = td.comment.presence
change_table_comment(table_name, table_comment)
end
@@ -499,6 +500,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by #create_table.
def drop_table(table_name, options = {})
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
end
@@ -518,14 +520,15 @@ module ActiveRecord
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
# Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column
- # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns.
+ # and number of bytes for <tt>:text</tt>, <tt>:binary</tt>, and <tt>:integer</tt> columns.
# This option is ignored by some backends.
# * <tt>:default</tt> -
# The column's default value. Use +nil+ for +NULL+.
# * <tt>:null</tt> -
# Allows or disallows +NULL+ values in the column.
# * <tt>:precision</tt> -
- # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # Specifies the precision for the <tt>:decimal</tt>, <tt>:numeric</tt>,
+ # <tt>:datetime</tt>, and <tt>:time</tt> columns.
# * <tt>:scale</tt> -
# Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
# * <tt>:collation</tt> -
@@ -1060,8 +1063,8 @@ module ActiveRecord
options
end
- def dump_schema_information #:nodoc:
- versions = ActiveRecord::SchemaMigration.all_versions
+ def dump_schema_information # :nodoc:
+ versions = schema_migration.all_versions
insert_versions_sql(versions) if versions.any?
end
@@ -1077,7 +1080,7 @@ module ActiveRecord
end
version = version.to_i
- sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
+ sm_table = quote_table_name(schema_migration.table_name)
migrated = migration_context.get_all_versions
versions = migration_context.migrations.map(&:version)
@@ -1450,7 +1453,7 @@ module ActiveRecord
end
def insert_versions_sql(versions)
- sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
+ sm_table = quote_table_name(schema_migration.table_name)
if versions.is_a?(Array)
sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 7b6321d83b..53ce8df491 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -5,10 +5,11 @@ module ActiveRecord
class TransactionState
def initialize(state = nil)
@state = state
- @children = []
+ @children = nil
end
def add_child(state)
+ @children ||= []
@children << state
end
@@ -41,12 +42,12 @@ module ActiveRecord
end
def rollback!
- @children.each { |c| c.rollback! }
+ @children&.each { |c| c.rollback! }
@state = :rolledback
end
def full_rollback!
- @children.each { |c| c.rollback! }
+ @children&.each { |c| c.rollback! }
@state = :fully_rolledback
end
@@ -75,18 +76,19 @@ module ActiveRecord
class Transaction #:nodoc:
attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
- def initialize(connection, options, run_commit_callbacks: false)
+ def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
@connection = connection
@state = TransactionState.new
- @records = []
- @isolation_level = options[:isolation]
+ @records = nil
+ @isolation_level = isolation
@materialized = false
- @joinable = options.fetch(:joinable, true)
+ @joinable = joinable
@run_commit_callbacks = run_commit_callbacks
end
def add_record(record)
- records << record
+ @records ||= []
+ @records << record
end
def materialize!
@@ -98,6 +100,7 @@ module ActiveRecord
end
def rollback_records
+ return unless records
ite = records.uniq(&:object_id)
already_run_callbacks = {}
while record = ite.shift
@@ -107,16 +110,17 @@ module ActiveRecord
record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
end
ensure
- ite.each do |i|
+ ite&.each do |i|
i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
end
end
def before_commit_records
- records.uniq.each(&:before_committed!) if @run_commit_callbacks
+ records.uniq.each(&:before_committed!) if records && @run_commit_callbacks
end
def commit_records
+ return unless records
ite = records.uniq(&:object_id)
already_run_callbacks = {}
while record = ite.shift
@@ -131,7 +135,7 @@ module ActiveRecord
end
end
ensure
- ite.each { |i| i.committed!(should_run_callbacks: false) }
+ ite&.each { |i| i.committed!(should_run_callbacks: false) }
end
def full_rollback?; true; end
@@ -141,8 +145,8 @@ module ActiveRecord
end
class SavepointTransaction < Transaction
- def initialize(connection, savepoint_name, parent_transaction, *args)
- super(connection, *args)
+ def initialize(connection, savepoint_name, parent_transaction, **options)
+ super(connection, options)
parent_transaction.state.add_child(@state)
@@ -202,18 +206,29 @@ module ActiveRecord
@lazy_transactions_enabled = true
end
- def begin_transaction(options = {})
+ def begin_transaction(isolation: nil, joinable: true, _lazy: true)
@connection.lock.synchronize do
run_commit_callbacks = !current_transaction.joinable?
transaction =
if @stack.empty?
- RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
+ RealTransaction.new(
+ @connection,
+ isolation: isolation,
+ joinable: joinable,
+ run_commit_callbacks: run_commit_callbacks
+ )
else
- SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options,
- run_commit_callbacks: run_commit_callbacks)
+ SavepointTransaction.new(
+ @connection,
+ "active_record_#{@stack.size}",
+ @stack.last,
+ isolation: isolation,
+ joinable: joinable,
+ run_commit_callbacks: run_commit_callbacks
+ )
end
- if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false
+ if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy
@has_unmaterialized_transactions = true
else
transaction.materialize!
@@ -274,9 +289,9 @@ module ActiveRecord
end
end
- def within_new_transaction(options = {})
+ def within_new_transaction(isolation: nil, joinable: true)
@connection.lock.synchronize do
- transaction = begin_transaction options
+ transaction = begin_transaction(isolation: isolation, joinable: joinable)
yield
rescue Exception => error
if transaction
@@ -309,7 +324,6 @@ module ActiveRecord
end
private
-
NULL_TRANSACTION = NullTransaction.new
# Deallocate invalidated prepared statements outside of the transaction
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index bf0bb84c93..dc970c384b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -6,7 +6,6 @@ require "active_record/connection_adapters/sql_type_metadata"
require "active_record/connection_adapters/abstract/schema_dumper"
require "active_record/connection_adapters/abstract/schema_creation"
require "active_support/concurrency/load_interlock_aware_monitor"
-require "active_support/deprecation"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
@@ -78,7 +77,7 @@ module ActiveRecord
SIMPLE_INT = /\A\d+\z/
attr_accessor :pool
- attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes
+ attr_reader :visitor, :owner, :logger, :lock, :prepared_statements
alias :in_use? :owner
set_callback :checkin, :after, :enable_lazy_transactions!
@@ -106,6 +105,14 @@ module ActiveRecord
Regexp.union(*parts)
end
+ def self.quoted_column_names # :nodoc:
+ @quoted_column_names ||= {}
+ end
+
+ def self.quoted_table_names # :nodoc:
+ @quoted_table_names ||= {}
+ end
+
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@@ -114,11 +121,8 @@ module ActiveRecord
@instrumenter = ActiveSupport::Notifications.instrumenter
@logger = logger
@config = config
- @pool = nil
+ @pool = ActiveRecord::ConnectionAdapters::NullPool.new
@idle_since = Concurrent.monotonic_time
- @schema_cache = SchemaCache.new self
- @quoted_column_names, @quoted_table_names = {}, {}
- @prevent_writes = false
@visitor = arel_visitor
@statements = build_statement_pool
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
@@ -144,19 +148,7 @@ module ActiveRecord
# Returns true if the connection is a replica, or if +prevent_writes+
# is set to true.
def preventing_writes?
- replica? || prevent_writes
- end
-
- # Prevent writing to the database regardless of role.
- #
- # In some cases you may want to prevent writes to the database
- # even if you are on a database that can write. `while_preventing_writes`
- # will prevent writes to the database for the duration of the block.
- def while_preventing_writes
- original, @prevent_writes = @prevent_writes, true
- yield
- ensure
- @prevent_writes = original
+ replica? || ActiveRecord::Base.connection_handler.prevent_writes
end
def migrations_paths # :nodoc:
@@ -164,7 +156,22 @@ module ActiveRecord
end
def migration_context # :nodoc:
- MigrationContext.new(migrations_paths)
+ MigrationContext.new(migrations_paths, schema_migration)
+ end
+
+ def schema_migration # :nodoc:
+ @schema_migration ||= begin
+ conn = self
+ spec_name = conn.pool.spec.name
+ name = "#{spec_name}::SchemaMigration"
+
+ Class.new(ActiveRecord::SchemaMigration) do
+ define_singleton_method(:name) { name }
+ define_singleton_method(:to_s) { name }
+
+ self.connection_specification_name = spec_name
+ end
+ end
end
class Version
@@ -206,9 +213,13 @@ module ActiveRecord
@owner = Thread.current
end
+ def schema_cache
+ @pool.get_schema_cache(self)
+ end
+
def schema_cache=(cache)
cache.connection = self
- @schema_cache = cache
+ @pool.set_schema_cache(cache)
end
# this method must only be called while holding connection pool's mutex
@@ -259,6 +270,11 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
+ # Does the database for this adapter exist?
+ def self.database_exists?(config)
+ raise NotImplementedError
+ end
+
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
@@ -487,6 +503,9 @@ module ActiveRecord
#
# Prevent @connection's finalizer from touching the socket, or
# otherwise communicating with its server, when it is collected.
+ if schema_cache.connection == self
+ schema_cache.connection = nil
+ end
end
# Reset the state of this connection, directing the DBMS to clear
@@ -587,7 +606,6 @@ module ActiveRecord
end
private
-
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping)
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 d239ecff89..ef1eef6b69 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -45,7 +45,6 @@ module ActiveRecord
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
-
def dealloc(stmt)
stmt.close
end
@@ -176,15 +175,6 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
- 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)
materialize_transactions
@@ -298,6 +288,8 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
@@ -318,6 +310,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -483,12 +476,12 @@ module ActiveRecord
# distinct queries, and requires that the ORDER BY include the distinct column.
# See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
def columns_for_distinct(columns, orders) # :nodoc:
- order_columns = orders.reject(&:blank?).map { |s|
+ order_columns = orders.compact_blank.map { |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
- }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
(order_columns << super).join(", ")
end
@@ -522,7 +515,6 @@ module ActiveRecord
end
private
-
def initialize_type_map(m = type_map)
super
@@ -627,7 +619,11 @@ module ActiveRecord
when ER_QUERY_INTERRUPTED
QueryCanceled.new(message, sql: sql, binds: binds)
else
- super
+ if exception.is_a?(Mysql2::Error::TimeoutError)
+ ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds)
+ else
+ super
+ end
end
end
@@ -745,7 +741,7 @@ module ActiveRecord
end.compact.join(", ")
# ...and send them all in one query
- execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
+ execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
end
def column_definitions(table_name) # :nodoc:
@@ -804,7 +800,6 @@ module ActiveRecord
end
private
-
def cast_value(value)
case value
when true then "1"
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 279d0b9e84..2708d2756b 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,6 +5,8 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
+ include Deduplicable
+
attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -76,6 +78,7 @@ module ActiveRecord
def hash
Column.hash ^
name.hash ^
+ name.encoding.hash ^
default.hash ^
sql_type_metadata.hash ^
null.hash ^
@@ -83,6 +86,17 @@ module ActiveRecord
collation.hash ^
comment.hash
end
+
+ private
+ def deduplicated
+ @name = -name
+ @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
+ @default = -default if default
+ @default_function = -default_function if default_function
+ @collation = -collation if collation
+ @comment = -comment if comment
+ super
+ end
end
class NullColumn < Column
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 9eaf9d9a89..0732b69f81 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -50,13 +50,12 @@ module ActiveRecord
# Converts the given URL to a full connection hash.
def to_hash
- config = raw_config.reject { |_, value| value.blank? }
+ config = raw_config.compact_blank
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
config
end
private
-
attr_reader :uri
def uri_parser
diff --git a/activerecord/lib/active_record/connection_adapters/deduplicable.rb b/activerecord/lib/active_record/connection_adapters/deduplicable.rb
new file mode 100644
index 0000000000..fb2fd60bbc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/deduplicable.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ module Deduplicable
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def registry
+ @registry ||= {}
+ end
+
+ def new(*)
+ super.deduplicate
+ end
+ end
+
+ def deduplicate
+ self.class.registry[self] ||= deduplicated
+ end
+ alias :-@ :deduplicate
+
+ private
+ def deduplicated
+ freeze
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
index 1df4dea2d8..97d74df529 100644
--- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module DetermineIfPreparableVisitor
attr_accessor :preparable
- def accept(*)
+ def accept(object, collector)
@preparable = true
super
end
@@ -20,7 +20,7 @@ module ActiveRecord
super
end
- def visit_Arel_Nodes_SqlLiteral(*)
+ def visit_Arel_Nodes_SqlLiteral(o, collector)
@preparable = false
super
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
index 2132e5d248..bbcdc96cdc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -26,6 +26,15 @@ module ActiveRecord
!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)
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_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index 234fb25fdf..bcd300f3db 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -41,13 +41,15 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
+ when /\A(?:enum|set)\b/
+ column.sql_type
else
super
end
end
def schema_limit(column)
- super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+ super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type)
end
def schema_precision(column)
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 9167593064..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,9 +6,11 @@ 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)
@extra = extra
end
@@ -25,6 +27,13 @@ module ActiveRecord
__getobj__.hash ^
extra.hash
end
+
+ private
+ def deduplicated
+ __setobj__(__getobj__.deduplicate)
+ @extra = -extra if extra
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 5b0335c22b..1df9ac32c9 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -8,6 +8,8 @@ require "mysql2"
module ActiveRecord
module ConnectionHandling # :nodoc:
+ ER_BAD_DB_ERROR = 1049
+
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
config = config.symbolize_keys
@@ -22,7 +24,7 @@ module ActiveRecord
client = Mysql2::Client.new(config)
ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config)
rescue Mysql2::Error => error
- if error.message.include?("Unknown database")
+ if error.error_number == ER_BAD_DB_ERROR
raise ActiveRecord::NoDatabaseError
else
raise
@@ -42,6 +44,12 @@ module ActiveRecord
configure_connection
end
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.mysql2_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
def supports_json?
!mariadb? && database_version >= "5.7.8"
end
@@ -109,12 +117,12 @@ module ActiveRecord
end
def discard! # :nodoc:
+ super
@connection.automatic_close = false
@connection = nil
end
private
-
def connect
@connection = Mysql2::Client.new(@config)
configure_connection
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index ec25bb1e19..f1ecf6df30 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -23,6 +23,29 @@ module ActiveRecord
def sql_type
super.sub(/\[\]\z/, "")
end
+
+ def init_with(coder)
+ @serial = coder["serial"]
+ super
+ end
+
+ def encode_with(coder)
+ coder["serial"] = @serial
+ super
+ end
+
+ def ==(other)
+ other.is_a?(Column) &&
+ super &&
+ serial? == other.serial?
+ end
+ alias :eql? :==
+
+ def hash
+ Column.hash ^
+ super.hash ^
+ serial?.hash
+ end
end
end
PostgreSQLColumn = PostgreSQL::Column # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index b1dfbde86e..0bbe98145a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -77,7 +77,6 @@ module ActiveRecord
end
private
-
def type_cast_array(value, method)
if value.is_a?(::Array)
value.map { |item| type_cast_array(item, method) }
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index f70f09ad95..bae34472e1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -10,7 +10,6 @@ module ActiveRecord
end
private
-
def cast_value(value)
value.to_s
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index 7b42677101..8d4dacbd64 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -46,7 +46,6 @@ module ActiveRecord
end
private
-
HstorePair = begin
quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
index 7f6adc351c..e52d4385ef 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
@@ -34,7 +34,6 @@ module ActiveRecord
end
private
-
def number_for_point(number)
number.to_s.gsub(/\.0$/, "")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 6434377b57..357493dfc0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -26,9 +26,9 @@ module ActiveRecord
value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ when /^-?\D*[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, "")
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ when /^-?\D*[\d.]+,\d{2}$/ # (2)
value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 8c74cecc4d..e81e18ff70 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -50,7 +50,6 @@ module ActiveRecord
end
private
-
def number_for_point(number)
number.to_s.gsub(/\.0$/, "")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index aa7701e038..d19f1f9cf8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -58,7 +58,6 @@ module ActiveRecord
end
private
-
def type_cast_single(value)
infinity?(value) ? value : @subtype.deserialize(value)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 28abdbd073..74a28eef58 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -14,7 +14,6 @@ module ActiveRecord
end
private
-
def cast_value(value)
casted = value.to_s
casted if casted.match?(ACCEPTABLE_UUID)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index d40e0ef1f0..07b66de366 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -30,7 +30,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name) # :nodoc:
- @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
end
# Quotes schema names for use in SQL queries.
@@ -44,7 +44,7 @@ module ActiveRecord
# Quotes column names for use in SQL queries.
def quote_column_name(name) # :nodoc:
- @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
+ self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
@@ -78,6 +78,43 @@ module ActiveRecord
type_map.lookup(column.oid, column.fmod, column.sql_type)
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"::type_name | function(one or no argument)::type_name
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
+ )
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ COLUMN_NAME_WITH_ORDER = /
+ \A
+ (
+ (?:
+ # "table_name"."column_name"::type_name | function(one or no argument)::type_name
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
+ )
+ (?:\s+ASC|\s+DESC)?
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
+ )
+ (?:\s*,\s*\g<1>)*
+ \z
+ /ix
+
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
+
private
def lookup_cast_type(sql_type)
super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
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 84643d20da..d201e40190 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -5,7 +5,6 @@ module ActiveRecord
module PostgreSQL
class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
private
-
def extensions(stream)
extensions = @connection.extensions
if extensions.any?
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 40c5e51d92..628a609521 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -55,6 +55,7 @@ module ActiveRecord
end
def drop_table(table_name, options = {}) # :nodoc:
+ schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -376,6 +377,8 @@ module ActiveRecord
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
clear_cache!
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
if pk
@@ -552,13 +555,13 @@ module ActiveRecord
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
def columns_for_distinct(columns, orders) #:nodoc:
- order_columns = orders.reject(&:blank?).map { |s|
+ order_columns = orders.compact_blank.map { |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
- }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
(order_columns << super).join(", ")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index 8bdec623af..b7f6479357 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -7,6 +7,8 @@ module ActiveRecord
class TypeMetadata < DelegateClass(SqlTypeMetadata)
undef to_yaml if method_defined?(:to_yaml)
+ include Deduplicable
+
attr_reader :oid, :fmod
def initialize(type_metadata, oid: nil, fmod: nil)
@@ -29,6 +31,12 @@ module ActiveRecord
oid.hash ^
fmod.hash
end
+
+ private
+ def deduplicated
+ __setobj__(__getobj__.deduplicate)
+ super
+ end
end
end
PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index f2f4701500..e8caeb8132 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -37,7 +37,6 @@ module ActiveRecord
end
protected
-
def parts
@parts ||= [@schema, @identifier].compact
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 91318a0af1..0a7c6d8ac4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -46,7 +46,7 @@ module ActiveRecord
conn = PG.connect(conn_params)
ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
rescue ::PG::Error => error
- if error.message.include?("does not exist")
+ if error.message.include?(conn_params[:dbname])
raise ActiveRecord::NoDatabaseError
else
raise
@@ -259,6 +259,12 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.postgresql_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
# Is this connection alive and ready for queries?
def active?
@lock.synchronize do
@@ -302,6 +308,7 @@ module ActiveRecord
end
def discard! # :nodoc:
+ super
@connection.socket_io.reopen(IO::NULL) rescue nil
@connection = nil
end
@@ -452,7 +459,6 @@ module ActiveRecord
end
private
-
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index dbfe1e4a34..5e30304864 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -27,7 +27,6 @@ module ActiveRecord
def encode_with(coder)
coder["columns"] = @columns
- coder["columns_hash"] = @columns_hash
coder["primary_keys"] = @primary_keys
coder["data_sources"] = @data_sources
coder["indexes"] = @indexes
@@ -37,16 +36,21 @@ module ActiveRecord
def init_with(coder)
@columns = coder["columns"]
- @columns_hash = coder["columns_hash"]
@primary_keys = coder["primary_keys"]
@data_sources = coder["data_sources"]
@indexes = coder["indexes"] || {}
@version = coder["version"]
@database_version = coder["database_version"]
+
+ derive_columns_hash_and_deduplicate_values
end
def primary_keys(table_name)
- @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys.fetch(table_name) do
+ if data_source_exists?(table_name)
+ @primary_keys[deep_deduplicate(table_name)] = deep_deduplicate(connection.primary_key(table_name))
+ end
+ end
end
# A cached lookup for table existence.
@@ -54,7 +58,7 @@ module ActiveRecord
prepare_data_sources if @data_sources.empty?
return @data_sources[name] if @data_sources.key? name
- @data_sources[name] = connection.data_source_exists?(name)
+ @data_sources[deep_deduplicate(name)] = connection.data_source_exists?(name)
end
# Add internal cache for table with +table_name+.
@@ -73,15 +77,17 @@ module ActiveRecord
# Get the columns for a table
def columns(table_name)
- @columns[table_name] ||= connection.columns(table_name)
+ @columns.fetch(table_name) do
+ @columns[deep_deduplicate(table_name)] = deep_deduplicate(connection.columns(table_name))
+ end
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
def columns_hash(table_name)
- @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
- [col.name, col]
- }]
+ @columns_hash.fetch(table_name) do
+ @columns_hash[deep_deduplicate(table_name)] = columns(table_name).index_by(&:name)
+ end
end
# Checks whether the columns hash is already cached for a table.
@@ -90,7 +96,9 @@ module ActiveRecord
end
def indexes(table_name)
- @indexes[table_name] ||= connection.indexes(table_name)
+ @indexes.fetch(table_name) do
+ @indexes[deep_deduplicate(table_name)] = deep_deduplicate(connection.indexes(table_name))
+ end
end
def database_version # :nodoc:
@@ -124,15 +132,38 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = connection.migration_context.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, database_version]
+ [@version, @columns, {}, @primary_keys, @data_sources, @indexes, database_version]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
- @indexes = @indexes || {}
+ @version, @columns, _columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
+ @indexes ||= {}
+
+ derive_columns_hash_and_deduplicate_values
end
private
+ def derive_columns_hash_and_deduplicate_values
+ @columns = deep_deduplicate(@columns)
+ @columns_hash = @columns.transform_values { |columns| columns.index_by(&:name) }
+ @primary_keys = deep_deduplicate(@primary_keys)
+ @data_sources = deep_deduplicate(@data_sources)
+ @indexes = deep_deduplicate(@indexes)
+ end
+
+ def deep_deduplicate(value)
+ case value
+ when Hash
+ value.transform_keys { |k| deep_deduplicate(k) }.transform_values { |v| deep_deduplicate(v) }
+ when Array
+ value.map { |i| deep_deduplicate(i) }
+ when String, Deduplicable
+ -value
+ else
+ value
+ end
+ end
+
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
end
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
index df28df7a7c..969867e70f 100644
--- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
+require "active_record/connection_adapters/deduplicable"
+
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
class SqlTypeMetadata
+ include Deduplicable
+
attr_reader :sql_type, :type, :limit, :precision, :scale
def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil)
@@ -32,6 +36,12 @@ module ActiveRecord
precision.hash >> 1 ^
scale.hash >> 2
end
+
+ private
+ def deduplicated
+ @sql_type = -sql_type
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
index 73b80e8c15..85053acf91 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -11,6 +11,11 @@ module ActiveRecord
!READ_QUERY.match?(sql)
end
+ def explain(arel, binds = [])
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
+ SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
+ end
+
def execute(sql, name = nil) #:nodoc:
if preventing_writes? && write_query?(sql)
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
@@ -79,7 +84,6 @@ module ActiveRecord
log("rollback transaction", "TRANSACTION") { @connection.rollback }
end
-
private
def execute_batch(sql, name = nil)
if preventing_writes? && write_query?(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index cb9d32a577..9b74a774e5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -13,11 +13,11 @@ module ActiveRecord
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
+ self.class.quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
end
def quote_column_name(name)
- @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ self.class.quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
def quoted_time(value)
@@ -45,8 +45,43 @@ module ActiveRecord
0
end
- private
+ 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 BigDecimal
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 41b35d9fc6..f4847eb6c0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -98,6 +98,16 @@ module ActiveRecord
configure_connection
end
+ def self.database_exists?(config)
+ config = config.symbolize_keys
+ if config[:database] == ":memory:"
+ return true
+ else
+ database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
+ File.exist?(database_file)
+ end
+ end
+
def supports_ddl_transactions?
true
end
@@ -201,14 +211,6 @@ module ActiveRecord
end
end
- #--
- # DATABASE STATEMENTS ======================================
- #++
- def explain(arel, binds = [])
- sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
- end
-
# SCHEMA STATEMENTS ========================================
def primary_keys(table_name) # :nodoc:
@@ -226,6 +228,8 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
+ schema_cache.clear_data_source_cache!(table_name.to_s)
+ schema_cache.clear_data_source_cache!(new_name.to_s)
exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
@@ -389,6 +393,7 @@ module ActiveRecord
if from_primary_key.is_a?(Array)
@definition.primary_keys from_primary_key
end
+
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
@@ -485,9 +490,9 @@ module ActiveRecord
result = exec_query(sql, "SCHEMA").first
if result
- # Splitting with left parentheses and picking up last will return all
+ # Splitting with left parentheses and discarding the first part will return all
# columns separated with comma(,).
- columns_string = result["sql"].split("(").last
+ columns_string = result["sql"].split("(", 2).last
columns_string.split(",").each do |column_string|
# This regex will match the column name and collation type and will save
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 46bd831da7..0960feed84 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -48,7 +48,6 @@ module ActiveRecord
end
private
-
def cache
@cache[Process.pid]
end