# frozen_string_literal: true require "mutex_m" require "active_support/core_ext/enumerable" module ActiveRecord # = Active Record Attribute Methods module AttributeMethods extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do initialize_generated_modules include Read include Write include BeforeTypeCast include Query include PrimaryKey include TimeZoneConversion include Dirty include Serialization end RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module # :nodoc: include Mutex_m end class << self def dangerous_attribute_methods # :nodoc: @dangerous_attribute_methods ||= ( Base.instance_methods + Base.private_instance_methods - Base.superclass.instance_methods - Base.superclass.private_instance_methods + %i[__id__ dup freeze frozen? hash class clone] ).map { |m| -m.to_s }.to_set.freeze end end module ClassMethods def initialize_generated_modules # :nodoc: @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new) private_constant :GeneratedAttributeMethods @attribute_methods_generated = false @alias_attributes_mass_generated = false include @generated_attribute_methods super end def alias_attribute(new_name, old_name) super if @alias_attributes_mass_generated ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator| generate_alias_attribute_methods(code_generator, new_name, old_name) end end end def eagerly_generate_alias_attribute_methods(_new_name, _old_name) # :nodoc: # alias attributes in Active Record are lazily generated end def generate_alias_attributes # :nodoc: superclass.generate_alias_attributes unless superclass == Base return false if @alias_attributes_mass_generated generated_attribute_methods.synchronize do return if @alias_attributes_mass_generated ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator| aliases_by_attribute_name.each do |old_name, new_names| new_names.each do |new_name| generate_alias_attribute_methods(code_generator, new_name, old_name) end end end @alias_attributes_mass_generated = true end true end def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc: attribute_method_patterns.each do |pattern| alias_attribute_method_definition(code_generator, pattern, new_name, old_name) end attribute_method_patterns_cache.clear end def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc: method_name = pattern.method_name(new_name).to_s target_name = pattern.method_name(old_name).to_s old_name = old_name.to_s method_defined = method_defined?(target_name) || private_method_defined?(target_name) manually_defined = method_defined && !self.instance_method(target_name).owner.is_a?(GeneratedAttributeMethods) reserved_method_name = ::ActiveRecord::AttributeMethods.dangerous_attribute_methods.include?(target_name) if !abstract_class? && !has_attribute?(old_name) # We only need to issue this deprecation warning once, so we issue it when defining the original reader method. should_warn = target_name == old_name if should_warn ActiveRecord.deprecator.warn( "#{self} model aliases `#{old_name}`, but `#{old_name}` is not an attribute. " \ "Starting in Rails 7.2, alias_attribute with non-attribute targets will raise. " \ "Use `alias_method :#{new_name}, :#{old_name}` or define the method manually." ) end super elsif manually_defined && !reserved_method_name aliased_method_redefined_as_well = method_defined_within?(method_name, self) return if aliased_method_redefined_as_well ActiveRecord.deprecator.warn( "#{self} model aliases `#{old_name}` and has a method called `#{target_name}` defined. " \ "Starting in Rails 7.2 `#{method_name}` will not be calling `#{target_name}` anymore. " \ "You may want to additionally define `#{method_name}` to preserve the current behavior." ) super else define_attribute_method_pattern(pattern, old_name, owner: code_generator, as: new_name, override: true) end end # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: return false if @attribute_methods_generated # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless base_class? super(attribute_names) @attribute_methods_generated = true end true end def attribute_methods_generated? # :nodoc: @attribute_methods_generated && @alias_attributes_mass_generated end def undefine_attribute_methods # :nodoc: generated_attribute_methods.synchronize do super if defined?(@attribute_methods_generated) && @attribute_methods_generated @attribute_methods_generated = false @alias_attributes_mass_generated = false end end # Raises an ActiveRecord::DangerousAttributeError exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base # def save # 'already defined by Active Record' # end # end # # Person.instance_method_already_implemented?(:save) # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # # Person.instance_method_already_implemented?(:name) # # => false def instance_method_already_implemented?(method_name) if dangerous_attribute_method?(method_name) raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name." end if superclass == Base super else # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass # defines its own attribute method, then we don't want to override that. defined = method_defined_within?(method_name, superclass, Base) && ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) defined || super end end # A method name is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) def dangerous_attribute_method?(name) # :nodoc: ::ActiveRecord::AttributeMethods.dangerous_attribute_methods.include?(name.to_s) end def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.method_defined?(name) || klass.private_method_defined?(name) if superklass.method_defined?(name) || superklass.private_method_defined?(name) klass.instance_method(name).owner != superklass.instance_method(name).owner else true end else false end end # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s) if Base.respond_to?(method_name, true) if Object.respond_to?(method_name, true) Base.method(method_name).owner != Object.method(method_name).owner else true end else false end end # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # # class Person < ActiveRecord::Base # end # # Person.attribute_method?('name') # => true # Person.attribute_method?(:age=) # => true # Person.attribute_method?(:nothing) # => false def attribute_method?(attribute) super || (table_exists? && column_names.include?(attribute.to_s.delete_suffix("="))) end # Returns an array of column names as strings if it's not an abstract class and # table exists. Otherwise it returns an empty array. # # class Person < ActiveRecord::Base # end # # Person.attribute_names # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? attribute_types.keys else [] end.freeze end # Returns true if the given attribute exists, otherwise false. # # class Person < ActiveRecord::Base # alias_attribute :new_name, :name # end # # Person.has_attribute?('name') # => true # Person.has_attribute?('new_name') # => true # Person.has_attribute?(:age) # => true # Person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) attr_name = attr_name.to_s attr_name = attribute_aliases[attr_name] || attr_name attribute_types.key?(attr_name) end def _has_attribute?(attr_name) # :nodoc: attribute_types.key?(attr_name) end private def inherited(child_class) super child_class.initialize_generated_modules child_class.class_eval do @alias_attributes_mass_generated = false @attribute_names = nil end end end # A Person object with a name attribute can ask person.respond_to?(:name), # person.respond_to?(:name=), and person.respond_to?(:name?) # which will all return +true+. It also defines the attribute methods if they have # not been generated. # # class Person < ActiveRecord::Base # end # # person = Person.new # person.respond_to?(:name) # => true # person.respond_to?(:name=) # => true # person.respond_to?(:name?) # => true # person.respond_to?('age') # => true # person.respond_to?('age=') # => true # person.respond_to?('age?') # => true # person.respond_to?(:nothing) # => false def respond_to?(name, include_private = false) return false unless super # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. if defined?(@attributes) if name = self.class.symbol_column_to_string(name.to_sym) return _has_attribute?(name) end end true end # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+. # # class Person < ActiveRecord::Base # alias_attribute :new_name, :name # end # # person = Person.new # person.has_attribute?(:name) # => true # person.has_attribute?(:new_name) # => true # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) attr_name = attr_name.to_s attr_name = self.class.attribute_aliases[attr_name] || attr_name @attributes.key?(attr_name) end def _has_attribute?(attr_name) # :nodoc: @attributes.key?(attr_name) end # Returns an array of names for the attributes available on this object. # # class Person < ActiveRecord::Base # end # # person = Person.new # person.attribute_names # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attributes.keys end # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. # # class Person < ActiveRecord::Base # end # # person = Person.create(name: 'Francesco', age: 22) # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes @attributes.to_hash end # Returns an #inspect-like string for the value of the # attribute +attr_name+. String attributes are truncated up to 50 # characters. Other attributes return the value of #inspect # without modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # # person.attribute_for_inspect(:name) # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\"" # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07.000000000 +0000\"" # # person.attribute_for_inspect(:tag_ids) # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) attr_name = attr_name.to_s attr_name = self.class.attribute_aliases[attr_name] || attr_name value = _read_attribute(attr_name) format_for_inspect(attr_name, value) end # Returns +true+ if the specified +attribute+ has been set by the user or by a # database load and is neither +nil+ nor empty? (the latter only applies # to objects that respond to empty?, most notably Strings). Otherwise, +false+. # Note that it always returns +true+ with boolean attributes. # # class Task < ActiveRecord::Base # end # # task = Task.new(title: '', is_done: false) # task.attribute_present?(:title) # => false # task.attribute_present?(:is_done) # => true # task.title = 'Buy milk' # task.is_done = true # task.attribute_present?(:title) # => true # task.attribute_present?(:is_done) # => true def attribute_present?(attr_name) attr_name = attr_name.to_s attr_name = self.class.attribute_aliases[attr_name] || attr_name value = _read_attribute(attr_name) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end # Returns the value of the attribute identified by +attr_name+ after it has # been type cast. (For information about specific type casting behavior, see # the types under ActiveModel::Type.) # # class Person < ActiveRecord::Base # belongs_to :organization # end # # person = Person.new(name: "Francesco", date_of_birth: "2004-12-12") # person[:name] # => "Francesco" # person[:date_of_birth] # => Date.new(2004, 12, 12) # person[:organization_id] # => nil # # Raises ActiveModel::MissingAttributeError if the attribute is missing. # Note, however, that the +id+ attribute will never be considered missing. # # person = Person.select(:name).first # person[:name] # => "Francesco" # person[:date_of_birth] # => ActiveModel::MissingAttributeError: missing attribute 'date_of_birth' for Person # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute 'organization_id' for Person # person[:id] # => nil def [](attr_name) read_attribute(attr_name) { |n| missing_attribute(n, caller) } end # Updates the attribute identified by +attr_name+ using the specified # +value+. The attribute value will be type cast upon being read. # # class Person < ActiveRecord::Base # end # # person = Person.new # person[:date_of_birth] = "2004-12-12" # person[:date_of_birth] # => Date.new(2004, 12, 12) def []=(attr_name, value) write_attribute(attr_name, value) end # Returns the name of all database fields which have been read from this # model. This can be useful in development mode to determine which fields # need to be selected. For performance critical pages, selecting only the # required fields can be an easy performance win (assuming you aren't using # all of the fields on the model). # # For example: # # class PostsController < ActionController::Base # after_action :print_accessed_fields, only: :index # # def index # @posts = Post.all # end # # private # def print_accessed_fields # p @posts.first.accessed_fields # end # end # # Which allows you to quickly change your code to: # # class PostsController < ActionController::Base # def index # @posts = Post.select(:id, :title, :author_id, :updated_at) # end # end def accessed_fields @attributes.accessed end private def respond_to_missing?(name, include_private = false) if self.class.define_attribute_methods # Some methods weren't defined yet. return true if self.class.method_defined?(name) return true if include_private && self.class.private_method_defined?(name) end super end def method_missing(name, ...) unless self.class.attribute_methods_generated? if self.class.method_defined?(name) # The method is explicitly defined in the model, but calls a generated # method with super. So we must resume the call chain at the right setp. last_method = method(name) last_method = last_method.super_method while last_method.super_method self.class.define_attribute_methods if last_method.super_method return last_method.super_method.call(...) end elsif self.class.define_attribute_methods | self.class.generate_alias_attributes # Some attribute methods weren't generated yet, we retry the call return public_send(name, ...) end end super end def attribute_method?(attr_name) # We check defined? because Syck calls respond_to? before actually calling initialize. defined?(@attributes) && @attributes.key?(attr_name) end def attributes_with_values(attribute_names) attribute_names.index_with { |name| @attributes[name] } end # Filters the primary keys, readonly attributes and virtual columns from the attribute names. def attributes_for_update(attribute_names) attribute_names &= self.class.column_names attribute_names.delete_if do |name| self.class.readonly_attribute?(name) || self.class.counter_cache_column?(name) || column_for_attribute(name).virtual? end end # Filters out the virtual columns and also primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) attribute_names &= self.class.column_names attribute_names.delete_if do |name| (pk_attribute?(name) && id.nil?) || column_for_attribute(name).virtual? end end def format_for_inspect(name, value) if value.nil? value.inspect else inspected_value = if value.is_a?(String) && value.length > 50 "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_fs(:inspect)}") else value.inspect end inspection_filter.filter_param(name, inspected_value) end end def pk_attribute?(name) name == @primary_key end end end