aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract_adapter.rb')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb681
1 files changed, 681 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
new file mode 100644
index 0000000000..346d4b067a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -0,0 +1,681 @@
+# frozen_string_literal: true
+
+require "active_record/connection_adapters/determine_if_preparable_visitor"
+require "active_record/connection_adapters/schema_cache"
+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 "arel/collectors/bind"
+require "arel/collectors/composite"
+require "arel/collectors/sql_string"
+require "arel/collectors/substitute_binds"
+
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ extend ActiveSupport::Autoload
+
+ autoload :Column
+ autoload :ConnectionSpecification
+
+ autoload_at "active_record/connection_adapters/abstract/schema_definitions" do
+ autoload :IndexDefinition
+ autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
+ autoload :ForeignKeyDefinition
+ autoload :TableDefinition
+ autoload :Table
+ autoload :AlterTable
+ autoload :ReferenceDefinition
+ end
+
+ autoload_at "active_record/connection_adapters/abstract/connection_pool" do
+ autoload :ConnectionHandler
+ end
+
+ autoload_under "abstract" do
+ autoload :SchemaStatements
+ autoload :DatabaseStatements
+ autoload :DatabaseLimits
+ autoload :Quoting
+ autoload :ConnectionPool
+ autoload :QueryCache
+ autoload :Savepoints
+ end
+
+ autoload_at "active_record/connection_adapters/abstract/transaction" do
+ autoload :TransactionManager
+ autoload :NullTransaction
+ autoload :RealTransaction
+ autoload :SavepointTransaction
+ autoload :TransactionState
+ end
+
+ # Active Record supports multiple database systems. AbstractAdapter and
+ # related classes form the abstraction layer which makes this possible.
+ # An AbstractAdapter represents a connection to a database, and provides an
+ # abstract interface for database-specific functionality such as establishing
+ # a connection, escaping values, building the right SQL fragments for +:offset+
+ # and +:limit+ options, etc.
+ #
+ # All the concrete database adapters follow the interface laid down in this class.
+ # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which
+ # you can use.
+ #
+ # Most of the methods in the adapter are useful during migrations. Most
+ # notably, the instance methods provided by SchemaStatements are very useful.
+ class AbstractAdapter
+ ADAPTER_NAME = "Abstract"
+ include ActiveSupport::Callbacks
+ define_callbacks :checkout, :checkin
+
+ include Quoting, DatabaseStatements, SchemaStatements
+ include DatabaseLimits
+ include QueryCache
+ include Savepoints
+
+ SIMPLE_INT = /\A\d+\z/
+
+ attr_accessor :visitor, :pool, :prevent_writes
+ attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
+ alias :in_use? :owner
+
+ set_callback :checkin, :after, :enable_lazy_transactions!
+
+ def self.type_cast_config_to_integer(config)
+ if config.is_a?(Integer)
+ config
+ elsif SIMPLE_INT.match?(config)
+ config.to_i
+ else
+ config
+ end
+ end
+
+ def self.type_cast_config_to_boolean(config)
+ if config == "false"
+ false
+ else
+ config
+ end
+ end
+
+ def self.build_read_query_regexp(*parts) # :nodoc:
+ parts = parts.map { |part| /\A\s*#{part}/i }
+ Regexp.union(*parts)
+ end
+
+ def initialize(connection, logger = nil, config = {}) # :nodoc:
+ super()
+
+ @connection = connection
+ @owner = nil
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ @logger = logger
+ @config = config
+ @pool = nil
+ @idle_since = Concurrent.monotonic_time
+ @schema_cache = SchemaCache.new self
+ @quoted_column_names, @quoted_table_names = {}, {}
+ @visitor = arel_visitor
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
+ @visitor.extend(DetermineIfPreparableVisitor)
+ else
+ @prepared_statements = false
+ end
+
+ @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
+ config.fetch(:advisory_locks, true)
+ )
+
+ check_version
+ end
+
+ def replica?
+ @config[:replica] || false
+ end
+
+ # Determines whether writes are currently being prevents.
+ #
+ # 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 = self.prevent_writes
+ self.prevent_writes = true
+ yield
+ ensure
+ self.prevent_writes = original
+ end
+
+ def migrations_paths # :nodoc:
+ @config[:migrations_paths] || Migrator.migrations_paths
+ end
+
+ def migration_context # :nodoc:
+ MigrationContext.new(migrations_paths)
+ end
+
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split(".").map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split(".").map(&:to_i)
+ end
+
+ def to_s
+ @version.join(".")
+ end
+ end
+
+ def valid_type?(type) # :nodoc:
+ !native_database_types[type].nil?
+ end
+
+ # this method must only be called while holding connection pool's mutex
+ def lease
+ if in_use?
+ msg = +"Cannot lease connection, "
+ if @owner == Thread.current
+ msg << "it is already leased by the current thread."
+ else
+ msg << "it is already in use by a different thread: #{@owner}. " \
+ "Current thread: #{Thread.current}."
+ end
+ raise ActiveRecordError, msg
+ end
+
+ @owner = Thread.current
+ end
+
+ def schema_cache=(cache)
+ cache.connection = self
+ @schema_cache = cache
+ end
+
+ # this method must only be called while holding connection pool's mutex
+ def expire
+ if in_use?
+ if @owner != Thread.current
+ raise ActiveRecordError, "Cannot expire connection, " \
+ "it is owned by a different thread: #{@owner}. " \
+ "Current thread: #{Thread.current}."
+ end
+
+ @idle_since = Concurrent.monotonic_time
+ @owner = nil
+ else
+ raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
+ end
+ end
+
+ # this method must only be called while holding connection pool's mutex (and a desire for segfaults)
+ def steal! # :nodoc:
+ if in_use?
+ if @owner != Thread.current
+ pool.send :remove_connection_from_thread_cache, self, @owner
+
+ @owner = Thread.current
+ end
+ else
+ raise ActiveRecordError, "Cannot steal connection, it is not currently leased."
+ end
+ end
+
+ # Seconds since this connection was returned to the pool
+ def seconds_idle # :nodoc:
+ return 0 if in_use?
+ Concurrent.monotonic_time - @idle_since
+ end
+
+ def unprepared_statement
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ yield
+ ensure
+ @prepared_statements = old_prepared_statements
+ end
+
+ # Returns the human-readable name of the adapter. Use mixed case - one
+ # can always use downcase if needed.
+ def adapter_name
+ self.class::ADAPTER_NAME
+ 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?
+ false
+ end
+
+ def supports_bulk_alter?
+ false
+ end
+
+ # Does this adapter support savepoints?
+ def supports_savepoints?
+ false
+ end
+
+ # Does this adapter support application-enforced advisory locking?
+ def supports_advisory_locks?
+ false
+ end
+
+ # Should primary key values be selected from their corresponding
+ # sequence before the insert statement? If true, next_sequence_value
+ # is called before each insert to set the record's primary key.
+ def prefetch_primary_key?(table_name = nil)
+ false
+ end
+
+ # Does this adapter support index sort order?
+ def supports_index_sort_order?
+ false
+ end
+
+ # Does this adapter support partial indices?
+ def supports_partial_index?
+ false
+ end
+
+ # Does this adapter support expression indices?
+ def supports_expression_index?
+ false
+ end
+
+ # Does this adapter support explain?
+ def supports_explain?
+ false
+ end
+
+ # Does this adapter support setting the isolation level for a transaction?
+ def supports_transaction_isolation?
+ false
+ end
+
+ # Does this adapter support database extensions?
+ def supports_extensions?
+ false
+ end
+
+ # Does this adapter support creating indexes in the same statement as
+ # creating the table?
+ def supports_indexes_in_create?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
+ # Does this adapter support creating invalid constraints?
+ def supports_validate_constraints?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints
+ # in the same statement as creating the table?
+ def supports_foreign_keys_in_create?
+ supports_foreign_keys?
+ end
+
+ # Does this adapter support views?
+ def supports_views?
+ false
+ end
+
+ # Does this adapter support materialized views?
+ def supports_materialized_views?
+ false
+ end
+
+ # Does this adapter support datetime with precision?
+ def supports_datetime_with_precision?
+ false
+ end
+
+ # Does this adapter support json data type?
+ def supports_json?
+ 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
+ deprecate :supports_multi_insert?
+
+ # Does this adapter support virtual columns?
+ def supports_virtual_columns?
+ false
+ end
+
+ # Does this adapter support foreign/external tables?
+ def supports_foreign_tables?
+ false
+ end
+
+ def supports_lazy_transactions?
+ false
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
+ def advisory_locks_enabled? # :nodoc:
+ supports_advisory_locks? && @advisory_locks_enabled
+ end
+
+ # This is meant to be implemented by the adapters that support advisory
+ # locks
+ #
+ # Return true if we got the lock, otherwise false
+ def get_advisory_lock(lock_id) # :nodoc:
+ end
+
+ # This is meant to be implemented by the adapters that support advisory
+ # locks.
+ #
+ # Return true if we released the lock, otherwise false
+ def release_advisory_lock(lock_id) # :nodoc:
+ end
+
+ # A list of extensions, to be filled in by adapters that support them.
+ def extensions
+ []
+ end
+
+ # A list of index algorithms, to be filled by adapters that support them.
+ def index_algorithms
+ {}
+ end
+
+ # REFERENTIAL INTEGRITY ====================================
+
+ # Override to turn off referential integrity while executing <tt>&block</tt>.
+ def disable_referential_integrity
+ yield
+ end
+
+ # CONNECTION MANAGEMENT ====================================
+
+ # Checks whether the connection to the database is still active. This includes
+ # checking whether the database is actually capable of responding, i.e. whether
+ # the connection isn't stale.
+ def active?
+ end
+
+ # Disconnects from the database if already connected, and establishes a
+ # new connection with the database. Implementors should call super if they
+ # override the default implementation.
+ def reconnect!
+ clear_cache!
+ reset_transaction
+ end
+
+ # Disconnects from the database if already connected. Otherwise, this
+ # method does nothing.
+ def disconnect!
+ clear_cache!
+ reset_transaction
+ end
+
+ # Immediately forget this connection ever existed. Unlike disconnect!,
+ # this will not communicate with the server.
+ #
+ # After calling this method, the behavior of all other methods becomes
+ # undefined. This is called internally just before a forked process gets
+ # rid of a connection that belonged to its parent.
+ def discard!
+ # This should be overridden by concrete adapters.
+ #
+ # Prevent @connection's finalizer from touching the socket, or
+ # otherwise communicating with its server, when it is collected.
+ end
+
+ # Reset the state of this connection, directing the DBMS to clear
+ # transactions and other connection-related server-side state. Usually a
+ # database-dependent operation.
+ #
+ # The default implementation does nothing; the implementation should be
+ # overridden by concrete adapters.
+ def reset!
+ # this should be overridden by concrete adapters
+ end
+
+ ###
+ # Clear any caching the database adapter may be doing, for example
+ # clearing the prepared statement cache. This is database specific.
+ def clear_cache!
+ # this should be overridden by concrete adapters
+ end
+
+ # Returns true if its required to reload the connection between requests for development mode.
+ def requires_reloading?
+ false
+ end
+
+ # Checks whether the connection to the database is still active (i.e. not stale).
+ # This is done under the hood by calling #active?. If the connection
+ # is no longer active, then this method will reconnect to the database.
+ def verify!
+ reconnect! unless active?
+ end
+
+ # Provides access to the underlying database driver for this adapter. For
+ # example, this method returns a Mysql2::Client object in case of Mysql2Adapter,
+ # and a PG::Connection object in case of PostgreSQLAdapter.
+ #
+ # This is useful for when you need to call a proprietary method such as
+ # PostgreSQL's lo_* methods.
+ def raw_connection
+ disable_lazy_transactions!
+ @connection
+ end
+
+ def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
+ table[attribute].eq(value)
+ end
+
+ def case_insensitive_comparison(table, attribute, column, value) # :nodoc:
+ if can_perform_case_insensitive_comparison_for?(column)
+ table[attribute].lower.eq(table.lower(value))
+ else
+ table[attribute].eq(value)
+ end
+ end
+
+ def can_perform_case_insensitive_comparison_for?(column)
+ true
+ end
+ private :can_perform_case_insensitive_comparison_for?
+
+ # Check the connection back in to the connection pool
+ def close
+ pool.checkin self
+ end
+
+ def column_name_for_operation(operation, node) # :nodoc:
+ visitor.compile(node)
+ end
+
+ def default_index_type?(index) # :nodoc:
+ index.using.nil?
+ end
+
+ private
+ def check_version
+ end
+
+ def type_map
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
+ end
+ end
+
+ def initialize_type_map(m = type_map)
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_precision m, %r(date)i, Type::Date
+ register_class_with_precision m, %r(time)i, Type::Time
+ register_class_with_precision m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, "binary"
+ m.alias_type %r(clob)i, "text"
+ m.alias_type %r(timestamp)i, "datetime"
+ m.alias_type %r(numeric)i, "decimal"
+ m.alias_type %r(number)i, "decimal"
+ m.alias_type %r(double)i, "float"
+
+ m.register_type %r(^json)i, Type::Json.new
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
+
+ def reload_type_map
+ type_map.clear
+ initialize_type_map
+ end
+
+ def register_class_with_limit(mapping, key, klass)
+ mapping.register_type(key) do |*args|
+ limit = extract_limit(args.last)
+ klass.new(limit: limit)
+ end
+ end
+
+ def register_class_with_precision(mapping, key, klass)
+ mapping.register_type(key) do |*args|
+ precision = extract_precision(args.last)
+ klass.new(precision: precision)
+ end
+ end
+
+ def extract_scale(sql_type)
+ case sql_type
+ when /\((\d+)\)/ then 0
+ when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
+ end
+
+ def extract_precision(sql_type)
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
+
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def translate_exception_class(e, sql, binds)
+ message = "#{e.class.name}: #{e.message}"
+
+ exception = translate_exception(
+ e, message: message, sql: sql, binds: binds
+ )
+ exception.set_backtrace e.backtrace
+ exception
+ end
+
+ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc:
+ @instrumenter.instrument(
+ "sql.active_record",
+ sql: sql,
+ name: name,
+ binds: binds,
+ type_casted_binds: type_casted_binds,
+ statement_name: statement_name,
+ connection_id: object_id,
+ connection: self) do
+ @lock.synchronize do
+ yield
+ end
+ rescue => e
+ raise translate_exception_class(e, sql, binds)
+ end
+ end
+
+ def translate_exception(exception, message:, sql:, binds:)
+ # override in derived class
+ case exception
+ when RuntimeError
+ exception
+ else
+ ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
+ end
+ end
+
+ def without_prepared_statement?(binds)
+ !prepared_statements || binds.empty?
+ end
+
+ def column_for(table_name, column_name)
+ column_name = column_name.to_s
+ columns(table_name).detect { |c| c.name == column_name } ||
+ raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
+ end
+
+ def collector
+ if prepared_statements
+ Arel::Collectors::Composite.new(
+ Arel::Collectors::SQLString.new,
+ Arel::Collectors::Bind.new,
+ )
+ else
+ Arel::Collectors::SubstituteBinds.new(
+ self,
+ Arel::Collectors::SQLString.new,
+ )
+ end
+ end
+
+ def arel_visitor
+ Arel::Visitors::ToSql.new(self)
+ end
+ end
+ end
+end