# frozen_string_literal: true
require "active_support/core_ext/enumerable"
require "active_support/core_ext/module/delegation"
require "active_support/parameter_filter"
require "concurrent/map"
module ActiveRecord
# = Active Record \Core
module Core
extend ActiveSupport::Concern
include ActiveModel::Access
included do
##
# :singleton-method:
#
# Accepts a logger conforming to the interface of Log4r or the default
# Ruby +Logger+ class, which is then passed on to any new database
# connections made. You can retrieve this logger by calling +logger+ on
# either an Active Record model class or an Active Record model instance.
class_attribute :logger, instance_writer: false
class_attribute :_destroy_association_async_job, instance_accessor: false, default: "ActiveRecord::DestroyAssociationAsyncJob"
# The job class used to destroy associations in the background.
def self.destroy_association_async_job
if _destroy_association_async_job.is_a?(String)
self._destroy_association_async_job = _destroy_association_async_job.constantize
end
_destroy_association_async_job
rescue NameError => error
raise NameError, "Unable to load destroy_association_async_job: #{error.message}"
end
singleton_class.alias_method :destroy_association_async_job=, :_destroy_association_async_job=
delegate :destroy_association_async_job, to: :class
##
# :singleton-method:
#
# Specifies the maximum number of records that will be destroyed in a
# single background job by the dependent: :destroy_async
# association option. When +nil+ (default), all dependent records will be
# destroyed in a single background job. If specified, the records to be
# destroyed will be split into multiple background jobs.
class_attribute :destroy_association_async_batch_size, instance_writer: false, instance_predicate: false, default: nil
##
# Contains the database configuration - as is typically stored in config/database.yml -
# as an ActiveRecord::DatabaseConfigurations object.
#
# For example, the following database.yml...
#
# development:
# adapter: sqlite3
# database: storage/development.sqlite3
#
# production:
# adapter: sqlite3
# database: storage/production.sqlite3
#
# ...would result in ActiveRecord::Base.configurations to look like this:
#
# #,
# #
# ]>
def self.configurations=(config)
@@configurations = ActiveRecord::DatabaseConfigurations.new(config)
end
self.configurations = {}
# Returns fully resolved ActiveRecord::DatabaseConfigurations object
def self.configurations
@@configurations
end
##
# :singleton-method:
# Force enumeration of all columns in SELECT statements.
# e.g. SELECT first_name, last_name FROM ... instead of SELECT * FROM ...
# This avoids +PreparedStatementCacheExpired+ errors when a column is added
# to the database while the app is running.
class_attribute :enumerate_columns_in_select_statements, instance_accessor: false, default: false
class_attribute :belongs_to_required_by_default, instance_accessor: false
class_attribute :strict_loading_by_default, instance_accessor: false, default: false
class_attribute :has_many_inversing, instance_accessor: false, default: false
class_attribute :run_commit_callbacks_on_first_saved_instances_in_transaction, instance_accessor: false, default: true
class_attribute :default_connection_handler, instance_writer: false
class_attribute :default_role, instance_writer: false
class_attribute :default_shard, instance_writer: false
class_attribute :shard_selector, instance_accessor: false, default: nil
def self.application_record_class? # :nodoc:
if ActiveRecord.application_record_class
self == ActiveRecord.application_record_class
else
if defined?(ApplicationRecord) && self == ApplicationRecord
true
end
end
end
self.filter_attributes = []
def self.connection_handler
ActiveSupport::IsolatedExecutionState[:active_record_connection_handler] || default_connection_handler
end
def self.connection_handler=(handler)
ActiveSupport::IsolatedExecutionState[:active_record_connection_handler] = handler
end
def self.asynchronous_queries_session # :nodoc:
asynchronous_queries_tracker.current_session
end
def self.asynchronous_queries_tracker # :nodoc:
ActiveSupport::IsolatedExecutionState[:active_record_asynchronous_queries_tracker] ||= \
AsynchronousQueriesTracker.new
end
# Returns the symbol representing the current connected role.
#
# ActiveRecord::Base.connected_to(role: :writing) do
# ActiveRecord::Base.current_role #=> :writing
# end
#
# ActiveRecord::Base.connected_to(role: :reading) do
# ActiveRecord::Base.current_role #=> :reading
# end
def self.current_role
connected_to_stack.reverse_each do |hash|
return hash[:role] if hash[:role] && hash[:klasses].include?(Base)
return hash[:role] if hash[:role] && hash[:klasses].include?(connection_class_for_self)
end
default_role
end
# Returns the symbol representing the current connected shard.
#
# ActiveRecord::Base.connected_to(role: :reading) do
# ActiveRecord::Base.current_shard #=> :default
# end
#
# ActiveRecord::Base.connected_to(role: :writing, shard: :one) do
# ActiveRecord::Base.current_shard #=> :one
# end
def self.current_shard
connected_to_stack.reverse_each do |hash|
return hash[:shard] if hash[:shard] && hash[:klasses].include?(Base)
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
end
default_shard
end
# Returns the symbol representing the current setting for
# preventing writes.
#
# ActiveRecord::Base.connected_to(role: :reading) do
# ActiveRecord::Base.current_preventing_writes #=> true
# end
#
# ActiveRecord::Base.connected_to(role: :writing) do
# ActiveRecord::Base.current_preventing_writes #=> false
# end
def self.current_preventing_writes
connected_to_stack.reverse_each do |hash|
return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].include?(Base)
return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].include?(connection_class_for_self)
end
false
end
def self.connected_to_stack # :nodoc:
if connected_to_stack = ActiveSupport::IsolatedExecutionState[:active_record_connected_to_stack]
connected_to_stack
else
connected_to_stack = Concurrent::Array.new
ActiveSupport::IsolatedExecutionState[:active_record_connected_to_stack] = connected_to_stack
connected_to_stack
end
end
def self.connection_class=(b) # :nodoc:
@connection_class = b
end
def self.connection_class # :nodoc:
@connection_class ||= false
end
def self.connection_class? # :nodoc:
self.connection_class
end
def self.connection_class_for_self # :nodoc:
klass = self
until klass == Base
break if klass.connection_class?
klass = klass.superclass
end
klass
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
self.default_role = ActiveRecord.writing_role
self.default_shard = :default
def self.strict_loading_violation!(owner:, reflection:) # :nodoc:
case ActiveRecord.action_on_strict_loading_violation
when :raise
message = reflection.strict_loading_violation_message(owner)
raise ActiveRecord::StrictLoadingViolationError.new(message)
when :log
name = "strict_loading_violation.active_record"
ActiveSupport::Notifications.instrument(name, owner: owner, reflection: reflection)
end
end
end
module ClassMethods
def initialize_find_by_cache # :nodoc:
@find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
end
def find(*ids) # :nodoc:
# We don't have cache keys for this stuff yet
return super unless ids.length == 1
return super if block_given? || primary_key.nil? || scope_attributes?
id = ids.first
return super if StatementCache.unsupported_value?(id)
cached_find_by([primary_key], [id]) ||
raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
end
def find_by(*args) # :nodoc:
return super if scope_attributes?
hash = args.first
return super unless Hash === hash
hash = hash.each_with_object({}) do |(key, value), h|
key = key.to_s
key = attribute_aliases[key] || key
return super if reflect_on_aggregation(key)
reflection = _reflect_on_association(key)
if !reflection
value = value.id if value.respond_to?(:id)
elsif reflection.belongs_to? && !reflection.polymorphic?
key = reflection.join_foreign_key
pkey = reflection.join_primary_key
if pkey.is_a?(Array)
if pkey.all? { |attribute| value.respond_to?(attribute) }
value = pkey.map do |attribute|
if attribute == "id"
value.id_value
else
value.public_send(attribute)
end
end
composite_primary_key = true
end
else
value = value.public_send(pkey) if value.respond_to?(pkey)
end
end
if !composite_primary_key &&
(!columns_hash.key?(key) || StatementCache.unsupported_value?(value))
return super
end
h[key] = value
end
cached_find_by(hash.keys, hash.values)
end
def find_by!(*args) # :nodoc:
find_by(*args) || where(*args).raise_record_not_found_exception!
end
def initialize_generated_modules # :nodoc:
generated_association_methods
end
def generated_association_methods # :nodoc:
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
private_constant :GeneratedAssociationMethods
include mod
mod
end
end
# Returns columns which shouldn't be exposed while calling +#inspect+.
def filter_attributes
if @filter_attributes.nil?
superclass.filter_attributes
else
@filter_attributes
end
end
# Specifies columns which shouldn't be exposed while calling +#inspect+.
def filter_attributes=(filter_attributes)
@inspection_filter = nil
@filter_attributes = filter_attributes
end
def inspection_filter # :nodoc:
if @filter_attributes.nil?
superclass.inspection_filter
else
@inspection_filter ||= begin
mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
ActiveSupport::ParameterFilter.new(@filter_attributes, mask: mask)
end
end
end
# Returns a string like 'Post(id:integer, title:string, body:text)'
def inspect # :nodoc:
if self == Base
super
elsif abstract_class?
"#{super}(abstract)"
elsif !connected?
"#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", "
"#{super}(#{attr_list})"
else
"#{super}(Table doesn't exist)"
end
end
# Returns an instance of +Arel::Table+ loaded with the current table name.
def arel_table # :nodoc:
@arel_table ||= Arel::Table.new(table_name, klass: self)
end
def predicate_builder # :nodoc:
@predicate_builder ||= PredicateBuilder.new(table_metadata)
end
def type_caster # :nodoc:
TypeCaster::Map.new(self)
end
def cached_find_by_statement(key, &block) # :nodoc:
cache = @find_by_statement_cache[connection.prepared_statements]
cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
end
private
def inherited(subclass)
super
# initialize cache at class definition for thread safety
subclass.initialize_find_by_cache
unless subclass.base_class?
klass = self
until klass.base_class?
klass.initialize_find_by_cache
klass = klass.superclass
end
end
subclass.class_eval do
@arel_table = nil
@predicate_builder = nil
@inspection_filter = nil
@filter_attributes = nil
@generated_association_methods = nil
end
end
def relation
relation = Relation.create(self)
if finder_needs_type_condition? && !ignore_default_scope?
relation.where!(type_condition)
else
relation
end
end
def table_metadata
TableMetadata.new(self, arel_table)
end
def cached_find_by(keys, values)
statement = cached_find_by_statement(keys) { |params|
wheres = keys.index_with do |key|
if key.is_a?(Array)
[key.map { params.bind }]
else
params.bind
end
end
where(wheres).limit(1)
}
begin
statement.execute(values.flatten, connection).first
rescue TypeError
raise ActiveRecord::StatementInvalid
end
end
end
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
# In both instances, valid attribute keys are determined by the column names of the associated table --
# hence you can't have attributes that aren't part of the table columns.
#
# ==== Example
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
@new_record = true
@attributes = self.class._default_attributes.deep_dup
init_internals
initialize_internals_callback
assign_attributes(attributes) if attributes
yield self if block_given?
_run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ should be
# the result of previously encoding an Active Record model, using
# #encode_with.
#
# class Post < ActiveRecord::Base
# end
#
# old_post = Post.new(title: "hello world")
# coder = {}
# old_post.encode_with(coder)
#
# post = Post.allocate
# post.init_with(coder)
# post.title # => 'hello world'
def init_with(coder, &block)
coder = LegacyYamlAdapter.convert(coder)
attributes = self.class.yaml_encoder.decode(coder)
init_with_attributes(attributes, coder["new_record"], &block)
end
##
# Initialize an empty model object from +attributes+.
# +attributes+ should be an attributes object, and unlike the
# `initialize` method, no assignment calls are made per attribute.
def init_with_attributes(attributes, new_record = false) # :nodoc:
@new_record = new_record
@attributes = attributes
init_internals
yield self if block_given?
_run_find_callbacks
_run_initialize_callbacks
self
end
##
# :method: clone
# Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
# That means that modifying attributes of the clone will modify the original, since they will both point to the
# same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
#
# user = User.first
# new_user = user.clone
# user.name # => "Bob"
# new_user.name = "Joe"
# user.name # => "Joe"
#
# user.object_id == new_user.object_id # => false
# user.name.object_id == new_user.name.object_id # => true
#
# user.name.object_id == user.dup.name.object_id # => false
##
# :method: dup
# Duped objects have no id assigned and are treated as new records. Note
# that this is a "shallow" copy as it copies the object's attributes
# only, not its associations. The extent of a "deep" copy is application
# specific and is therefore left to the application to implement according
# to its need.
# The dup method does not preserve the timestamps (created|updated)_(at|on)
# and locking column.
##
def initialize_dup(other) # :nodoc:
@attributes = @attributes.deep_dup
if self.class.composite_primary_key?
@primary_key.each { |key| @attributes.reset(key) }
else
@attributes.reset(@primary_key)
end
_run_initialize_callbacks
@new_record = true
@previously_new_record = false
@destroyed = false
@_start_transaction_state = nil
super
end
# Populate +coder+ with attributes about this record that should be
# serialized. The structure of +coder+ defined in this method is
# guaranteed to match the structure of +coder+ passed to the #init_with
# method.
#
# Example:
#
# class Post < ActiveRecord::Base
# end
# coder = {}
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
self.class.yaml_encoder.encode(@attributes, coder)
coder["new_record"] = new_record?
coder["active_record_yaml_version"] = 2
end
##
# :method: slice
#
# :call-seq: slice(*methods)
#
# Returns a hash of the given methods with their names as keys and returned
# values as values.
#
# topic = Topic.new(title: "Budget", author_name: "Jason")
# topic.slice(:title, :author_name)
# => { "title" => "Budget", "author_name" => "Jason" }
#
#--
# Implemented by ActiveModel::Access#slice.
##
# :method: values_at
#
# :call-seq: values_at(*methods)
#
# Returns an array of the values returned by the given methods.
#
# topic = Topic.new(title: "Budget", author_name: "Jason")
# topic.values_at(:title, :author_name)
# => ["Budget", "Jason"]
#
#--
# Implemented by ActiveModel::Access#values_at.
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
#
# Note that new records are different from any other record by definition, unless the
# other record is the receiver itself. Besides, if you fetch existing records with
# +select+ and leave the ID out, you're on your own, this predicate will return false.
#
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
primary_key_values_present? &&
comparison_object.id == id
end
alias :eql? :==
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
id = self.id
if primary_key_values_present?
self.class.hash ^ id.hash
else
super
end
end
# Clone and freeze the attributes hash such that associations are still
# accessible, even on destroyed records, but cloned models will not be
# frozen.
def freeze
@attributes = @attributes.clone.freeze
self
end
# Returns +true+ if the attributes hash has been frozen.
def frozen?
@attributes.frozen?
end
# Allows sort on objects
def <=>(other_object)
if other_object.is_a?(self.class)
to_key <=> other_object.to_key
else
super
end
end
def present? # :nodoc:
true
end
def blank? # :nodoc:
false
end
# Returns +true+ if the record is read only.
def readonly?
@readonly
end
# Returns +true+ if the record is in strict_loading mode.
def strict_loading?
@strict_loading
end
# Sets the record to strict_loading mode. This will raise an error
# if the record tries to lazily load an association.
#
# user = User.first
# user.strict_loading! # => true
# user.address.city
# => ActiveRecord::StrictLoadingViolationError
# user.comments.to_a
# => ActiveRecord::StrictLoadingViolationError
#
# ==== Parameters
#
# * +value+ - Boolean specifying whether to enable or disable strict loading.
# * :mode - Symbol specifying strict loading mode. Defaults to :all. Using
# :n_plus_one_only mode will only raise an error if an association that
# will lead to an n plus one query is lazily loaded.
#
# ==== Examples
#
# user = User.first
# user.strict_loading!(false) # => false
# user.address.city # => "Tatooine"
# user.comments.to_a # => [# "Tatooine"
# user.comments.to_a # => [# ActiveRecord::StrictLoadingViolationError
def strict_loading!(value = true, mode: :all)
unless [:all, :n_plus_one_only].include?(mode)
raise ArgumentError, "The :mode option must be one of [:all, :n_plus_one_only] but #{mode.inspect} was provided."
end
@strict_loading_mode = mode
@strict_loading = value
end
attr_reader :strict_loading_mode
# Returns +true+ if the record uses strict_loading with +:n_plus_one_only+ mode enabled.
def strict_loading_n_plus_one_only?
@strict_loading_mode == :n_plus_one_only
end
# Marks this record as read only.
#
# customer = Customer.first
# customer.readonly!
# customer.save # Raises an ActiveRecord::ReadOnlyRecord
def readonly!
@readonly = true
end
def connection_handler
self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
def inspect
# We check defined?(@attributes) not to issue warnings if the object is
# allocated but not initialized.
inspection = if defined?(@attributes) && @attributes
attribute_names.filter_map do |name|
if _has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
end.join(", ")
else
"not initialized"
end
"#<#{self.class} #{inspection}>"
end
# Takes a PP and prettily prints this record to it, allowing you to get a nice result from pp record
# when pp is required.
def pretty_print(pp)
return super if custom_inspect_method_defined?
pp.object_address_group(self) do
if defined?(@attributes) && @attributes
attr_names = self.class.attribute_names.select { |name| _has_attribute?(name) }
pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
pp.breakable " "
pp.group(1) do
pp.text attr_name
pp.text ":"
pp.breakable
value = _read_attribute(attr_name)
value = inspection_filter.filter_param(attr_name, value) unless value.nil?
pp.pp value
end
end
else
pp.breakable " "
pp.text "not initialized"
end
end
end
private
# +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
# the array, and then rescues from the possible +NoMethodError+. If those elements are
# +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
# which significantly impacts upon performance.
#
# So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
#
# See also https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
def to_ary
nil
end
def init_internals
@readonly = false
@previously_new_record = false
@destroyed = false
@marked_for_destruction = false
@destroyed_by_association = nil
@_start_transaction_state = nil
klass = self.class
@primary_key = klass.primary_key
@strict_loading = klass.strict_loading_by_default
@strict_loading_mode = :all
klass.define_attribute_methods
klass.generate_alias_attributes
end
def initialize_internals_callback
end
def custom_inspect_method_defined?
self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
end
class InspectionMask < DelegateClass(::String)
def pretty_print(pp)
pp.text __getobj__
end
end
private_constant :InspectionMask
def inspection_filter
self.class.inspection_filter
end
end
end