aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/core.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/core.rb')
-rw-r--r--activerecord/lib/active_record/core.rb597
1 files changed, 597 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
new file mode 100644
index 0000000000..600825659b
--- /dev/null
+++ b/activerecord/lib/active_record/core.rb
@@ -0,0 +1,597 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/string/filters"
+require "active_support/parameter_filter"
+require "concurrent/map"
+
+module ActiveRecord
+ module Core
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ #
+ # Accepts a logger conforming to the interface of Log4r which is then
+ # passed on to any new database connections made and which can be
+ # retrieved on both a class and instance level by calling +logger+.
+ mattr_accessor :logger, instance_writer: false
+
+ ##
+ # :singleton-method:
+ #
+ # Specifies if the methods calling database queries should be logged below
+ # their relevant queries. Defaults to false.
+ mattr_accessor :verbose_query_logs, instance_writer: false, default: false
+
+ ##
+ # 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: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
+ # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production",
+ # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}>
+ # ]>
+ 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:
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
+ # dates and times from the database. This is set to :utc by default.
+ mattr_accessor :default_timezone, instance_writer: false, default: :utc
+
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ mattr_accessor :schema_format, instance_writer: false, default: :ruby
+
+ ##
+ # :singleton-method:
+ # Specifies if an error should be raised if the query has an order being
+ # ignored when doing batch queries. Useful in applications where the
+ # scope being ignored is error-worthy, rather than a warning.
+ mattr_accessor :error_on_ignored_order, instance_writer: false, default: false
+
+ # :singleton-method:
+ # Specify the behavior for unsafe raw query methods. Values are as follows
+ # deprecated - Warnings are logged when unsafe raw SQL is passed to
+ # query methods.
+ # disabled - Unsafe raw SQL passed to query methods results in
+ # UnknownAttributeReference exception.
+ mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated
+
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ mattr_accessor :timestamped_migrations, instance_writer: false, default: true
+
+ ##
+ # :singleton-method:
+ # Specify whether schema dump should happen at the end of the
+ # db:migrate rails command. This is true by default, which is useful for the
+ # development environment. This should ideally be false in the production
+ # environment where dumping schema is rarely needed.
+ mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
+
+ ##
+ # :singleton-method:
+ # Specifies which database schemas to dump when calling db:structure:dump.
+ # If the value is :schema_search_path (the default), any schemas listed in
+ # schema_search_path are dumped. Use :all to dump all schemas regardless
+ # of schema_search_path, or a string of comma separated schemas for a
+ # custom list.
+ mattr_accessor :dump_schemas, instance_writer: false, default: :schema_search_path
+
+ ##
+ # :singleton-method:
+ # Specify a threshold for the size of query result sets. If the number of
+ # records in the set exceeds the threshold, a warning is logged. This can
+ # be used to identify queries which load thousands of records and
+ # potentially cause memory bloat.
+ mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false
+
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ mattr_accessor :belongs_to_required_by_default, instance_accessor: false
+
+ mattr_accessor :connection_handlers, instance_accessor: false, default: {}
+
+ class_attribute :default_connection_handler, instance_writer: false
+
+ self.filter_attributes = []
+
+ def self.connection_handler
+ Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler
+ end
+
+ def self.connection_handler=(handler)
+ Thread.current.thread_variable_set("ar_connection_handler", handler)
+ end
+
+ self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
+ self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ module ClassMethods
+ def initialize_find_by_cache # :nodoc:
+ @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
+ end
+
+ def inherited(child_class) # :nodoc:
+ # initialize cache at class definition for thread safety
+ child_class.initialize_find_by_cache
+ super
+ 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? ||
+ columns_hash.include?(inheritance_column)
+
+ id = ids.first
+
+ return super if StatementCache.unsupported_value?(id)
+
+ key = primary_key
+
+ statement = cached_find_by_statement(key) { |params|
+ where(key => params.bind).limit(1)
+ }
+
+ record = statement.execute([id], connection).first
+ unless record
+ raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
+ name, primary_key, id)
+ end
+ record
+ rescue ::RangeError
+ raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'",
+ name, primary_key)
+ end
+
+ def find_by(*args) # :nodoc:
+ return super if scope_attributes? || reflect_on_all_aggregations.any?
+
+ hash = args.first
+
+ return super if !(Hash === hash) || hash.values.any? { |v|
+ StatementCache.unsupported_value?(v)
+ }
+
+ # We can't cache Post.find_by(author: david) ...yet
+ return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
+
+ keys = hash.keys
+
+ statement = cached_find_by_statement(keys) { |params|
+ wheres = keys.each_with_object({}) { |param, o|
+ o[param] = params.bind
+ }
+ where(wheres).limit(1)
+ }
+ begin
+ statement.execute(hash.values, connection).first
+ rescue TypeError
+ raise ActiveRecord::StatementInvalid
+ rescue ::RangeError
+ nil
+ end
+ end
+
+ def find_by!(*args) # :nodoc:
+ find_by(*args) || raise(RecordNotFound.new("Couldn't find #{name}", name))
+ 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 defined?(@filter_attributes)
+ @filter_attributes
+ else
+ superclass.filter_attributes
+ end
+ end
+
+ # Specifies columns which shouldn't be exposed while calling +#inspect+.
+ attr_writer :filter_attributes
+
+ # 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
+
+ # Overwrite the default class equality method to provide support for decorated models.
+ def ===(object) # :nodoc:
+ object.is_a?(self)
+ end
+
+ # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
+ #
+ # class Post < ActiveRecord::Base
+ # scope :published_and_commented, -> { published.and(arel_table[:comments_count].gt(0)) }
+ # end
+ def arel_table # :nodoc:
+ @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
+ end
+
+ def arel_attribute(name, table = arel_table) # :nodoc:
+ name = attribute_alias(name) if attribute_alias?(name)
+ table[name]
+ end
+
+ def predicate_builder # :nodoc:
+ @predicate_builder ||= PredicateBuilder.new(table_metadata)
+ end
+
+ def type_caster # :nodoc:
+ TypeCaster::Map.new(self)
+ end
+
+ def _internal? # :nodoc:
+ false
+ end
+
+ private
+
+ def cached_find_by_statement(key, &block)
+ cache = @find_by_statement_cache[connection.prepared_statements]
+ cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
+ end
+
+ def relation
+ relation = Relation.create(self)
+
+ if finder_needs_type_condition? && !ignore_default_scope?
+ relation.where!(type_condition)
+ relation.create_with!(inheritance_column.to_s => sti_name)
+ else
+ relation
+ end
+ end
+
+ def table_metadata
+ TableMetadata.new(self, arel_table)
+ 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)
+ self.class.define_attribute_methods
+ @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(self.class, 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:
+ init_internals
+
+ @new_record = new_record
+ @attributes = attributes
+
+ self.class.define_attribute_methods
+
+ 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).
+
+ ##
+ def initialize_dup(other) # :nodoc:
+ @attributes = @attributes.deep_dup
+ @attributes.reset(self.class.primary_key)
+
+ _run_initialize_callbacks
+
+ @new_record = true
+ @destroyed = false
+ @_start_transaction_state = {}
+ @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
+
+ # 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) &&
+ !id.nil? &&
+ 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
+ if id
+ 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
+
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
+ # attributes will be marked as read only since they cannot be saved.
+ def readonly?
+ @readonly
+ end
+
+ # Marks this record as read only.
+ 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
+ self.class.attribute_names.collect do |name|
+ if has_attribute?(name)
+ attr = _read_attribute(name)
+ value = if attr.nil?
+ attr.inspect
+ else
+ attr = format_for_inspect(attr)
+ inspection_filter.filter_param(name, attr)
+ end
+ "#{name}: #{value}"
+ end
+ end.compact.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 <tt>pp record</tt>
+ # 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
+
+ # Returns a hash of the given methods with their names as keys and returned values as values.
+ def slice(*methods)
+ Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access
+ 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
+ @destroyed = false
+ @marked_for_destruction = false
+ @destroyed_by_association = nil
+ @new_record = true
+ @_start_transaction_state = {}
+ @transaction_state = nil
+ end
+
+ def initialize_internals_callback
+ end
+
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
+ end
+
+ def custom_inspect_method_defined?
+ self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
+ end
+
+ def inspection_filter
+ @inspection_filter ||= begin
+ mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
+ def mask.pretty_print(pp)
+ pp.text __getobj__
+ end
+ ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
+ end
+ end
+ end
+end