aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attribute_methods
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/attribute_methods')
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb78
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb188
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb144
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb42
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb53
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb90
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb91
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb67
8 files changed, 753 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
new file mode 100644
index 0000000000..5941f51a1a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ # = Active Record Attribute Methods Before Type Cast
+ #
+ # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to
+ # read the value of the attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.id # => 1
+ # task.completed_on # => Sun, 21 Oct 2012
+ #
+ # task.attributes_before_type_cast
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
+ # task.read_attribute_before_type_cast('id') # => "1"
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ #
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
+ # suffix.
+ #
+ # task.id_before_type_cast # => "1"
+ # task.completed_on_before_type_cast # => "2012-10-21"
+ module BeforeTypeCast
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "_before_type_cast"
+ attribute_method_suffix "_came_from_user?"
+ end
+
+ # Returns the value of the attribute identified by +attr_name+ before
+ # typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.read_attribute('id') # => 1
+ # task.read_attribute_before_type_cast('id') # => '1'
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
+ def read_attribute_before_type_cast(attr_name)
+ @attributes[attr_name.to_s].value_before_type_cast
+ end
+
+ # Returns a hash of attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
+ # task.attributes
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
+ # task.attributes_before_type_cast
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
+ def attributes_before_type_cast
+ @attributes.values_before_type_cast
+ end
+
+ private
+
+ # Handle *_before_type_cast for method_missing.
+ def attribute_before_type_cast(attribute_name)
+ read_attribute_before_type_cast(attribute_name)
+ end
+
+ def attribute_came_from_user?(attribute_name)
+ @attributes[attribute_name].came_from_user?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
new file mode 100644
index 0000000000..45e4b8adfa
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+
+module ActiveRecord
+ module AttributeMethods
+ module Dirty
+ extend ActiveSupport::Concern
+
+ include ActiveModel::Dirty
+
+ included do
+ if self < ::ActiveRecord::Timestamp
+ raise "You cannot include Dirty after Timestamp"
+ end
+
+ class_attribute :partial_writes, instance_writer: false, default: true
+
+ # Attribute methods for "changed in last call to save?"
+ attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
+ attribute_method_prefix("saved_change_to_")
+ attribute_method_suffix("_before_last_save")
+
+ # Attribute methods for "will change if I call save?"
+ attribute_method_affix(prefix: "will_save_change_to_", suffix: "?")
+ attribute_method_suffix("_change_to_be_saved", "_in_database")
+ end
+
+ # <tt>reload</tt> the record and clears changed attributes.
+ def reload(*)
+ super.tap do
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_from_database = nil
+ end
+ end
+
+ # Did this attribute change when we last saved?
+ #
+ # This method is useful in after callbacks to determine if an attribute
+ # was changed during the save that triggered the callbacks to run. It can
+ # be invoked as +saved_change_to_name?+ instead of
+ # <tt>saved_change_to_attribute?("name")</tt>.
+ #
+ # ==== Options
+ #
+ # +from+ When passed, this method will return false unless the original
+ # value is equal to the given option
+ #
+ # +to+ When passed, this method will return false unless the value was
+ # changed to the given value
+ def saved_change_to_attribute?(attr_name, **options)
+ mutations_before_last_save.changed?(attr_name, **options)
+ end
+
+ # Returns the change to an attribute during the last save. If the
+ # attribute was changed, the result will be an array containing the
+ # original value and the saved value.
+ #
+ # This method is useful in after callbacks, to see the change in an
+ # attribute during the save that triggered the callbacks to run. It can be
+ # invoked as +saved_change_to_name+ instead of
+ # <tt>saved_change_to_attribute("name")</tt>.
+ def saved_change_to_attribute(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name)
+ end
+
+ # Returns the original value of an attribute before the last save.
+ #
+ # This method is useful in after callbacks to get the original value of an
+ # attribute before the save that triggered the callbacks to run. It can be
+ # invoked as +name_before_last_save+ instead of
+ # <tt>attribute_before_last_save("name")</tt>.
+ def attribute_before_last_save(attr_name)
+ mutations_before_last_save.original_value(attr_name)
+ end
+
+ # Did the last call to +save+ have any changes to change?
+ def saved_changes?
+ mutations_before_last_save.any_changes?
+ end
+
+ # Returns a hash containing all the changes that were just saved.
+ def saved_changes
+ mutations_before_last_save.changes
+ end
+
+ # Will this attribute change the next time we save?
+ #
+ # This method is useful in validations and before callbacks to determine
+ # if the next call to +save+ will change a particular attribute. It can be
+ # invoked as +will_save_change_to_name?+ instead of
+ # <tt>will_save_change_to_attribute("name")</tt>.
+ #
+ # ==== Options
+ #
+ # +from+ When passed, this method will return false unless the original
+ # value is equal to the given option
+ #
+ # +to+ When passed, this method will return false unless the value will be
+ # changed to the given value
+ def will_save_change_to_attribute?(attr_name, **options)
+ mutations_from_database.changed?(attr_name, **options)
+ end
+
+ # Returns the change to an attribute that will be persisted during the
+ # next save.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # change to an attribute that will occur when the record is saved. It can
+ # be invoked as +name_change_to_be_saved+ instead of
+ # <tt>attribute_change_to_be_saved("name")</tt>.
+ #
+ # If the attribute will change, the result will be an array containing the
+ # original value and the new value about to be saved.
+ def attribute_change_to_be_saved(attr_name)
+ mutations_from_database.change_to_attribute(attr_name)
+ end
+
+ # Returns the value of an attribute in the database, as opposed to the
+ # in-memory value that will be persisted the next time the record is
+ # saved.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # original value of an attribute prior to any changes about to be
+ # saved. It can be invoked as +name_in_database+ instead of
+ # <tt>attribute_in_database("name")</tt>.
+ def attribute_in_database(attr_name)
+ mutations_from_database.original_value(attr_name)
+ end
+
+ # Will the next call to +save+ have any changes to persist?
+ def has_changes_to_save?
+ mutations_from_database.any_changes?
+ end
+
+ # Returns a hash containing all the changes that will be persisted during
+ # the next save.
+ def changes_to_save
+ mutations_from_database.changes
+ end
+
+ # Returns an array of the names of any attributes that will change when
+ # the record is next saved.
+ def changed_attribute_names_to_save
+ mutations_from_database.changed_attribute_names
+ end
+
+ # Returns a hash of the attributes that will change when the record is
+ # next saved.
+ #
+ # The hash keys are the attribute names, and the hash values are the
+ # original attribute values in the database (as opposed to the in-memory
+ # values about to be saved).
+ def attributes_in_database
+ mutations_from_database.changed_values
+ end
+
+ private
+ def write_attribute_without_type_cast(attr_name, value)
+ name = attr_name.to_s
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
+ result = super(name, value)
+ clear_attribute_change(name)
+ result
+ end
+
+ def _update_record(attribute_names = attribute_names_for_partial_writes)
+ affected_rows = super
+ changes_applied
+ affected_rows
+ end
+
+ def _create_record(attribute_names = attribute_names_for_partial_writes)
+ id = super
+ changes_applied
+ id
+ end
+
+ def attribute_names_for_partial_writes
+ partial_writes? ? changed_attribute_names_to_save : attribute_names
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
new file mode 100644
index 0000000000..6af5346fa7
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActiveRecord
+ module AttributeMethods
+ module PrimaryKey
+ extend ActiveSupport::Concern
+
+ # Returns this record's primary key value wrapped in an array if one is
+ # available.
+ def to_key
+ key = id
+ [key] if key
+ end
+
+ # Returns the primary key column's value.
+ def id
+ sync_with_transaction_state
+ primary_key = self.class.primary_key
+ _read_attribute(primary_key) if primary_key
+ end
+
+ # Sets the primary key column's value.
+ def id=(value)
+ sync_with_transaction_state
+ primary_key = self.class.primary_key
+ _write_attribute(primary_key, value) if primary_key
+ end
+
+ # Queries the primary key column's value.
+ def id?
+ sync_with_transaction_state
+ query_attribute(self.class.primary_key)
+ end
+
+ # Returns the primary key column's value before type cast.
+ def id_before_type_cast
+ sync_with_transaction_state
+ read_attribute_before_type_cast(self.class.primary_key)
+ end
+
+ # Returns the primary key column's previous value.
+ def id_was
+ sync_with_transaction_state
+ attribute_was(self.class.primary_key)
+ end
+
+ # Returns the primary key column's value from the database.
+ def id_in_database
+ sync_with_transaction_state
+ attribute_in_database(self.class.primary_key)
+ end
+
+ private
+
+ def attribute_method?(attr_name)
+ attr_name == "id" || super
+ end
+
+ module ClassMethods
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set
+
+ def instance_method_already_implemented?(method_name)
+ super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name)
+ end
+
+ def dangerous_attribute_method?(method_name)
+ super && !ID_ATTRIBUTE_METHODS.include?(method_name)
+ end
+
+ # Defines the primary key field -- can be overridden in subclasses.
+ # Overwriting will negate any effect of the +primary_key_prefix_type+
+ # setting, though.
+ def primary_key
+ @primary_key = reset_primary_key unless defined? @primary_key
+ @primary_key
+ end
+
+ # Returns a quoted version of the primary key name, used to construct
+ # SQL statements.
+ def quoted_primary_key
+ @quoted_primary_key ||= connection.quote_column_name(primary_key)
+ end
+
+ def reset_primary_key #:nodoc:
+ if base_class?
+ self.primary_key = get_primary_key(base_class.name)
+ else
+ self.primary_key = base_class.primary_key
+ end
+ end
+
+ def get_primary_key(base_name) #:nodoc:
+ if base_name && primary_key_prefix_type == :table_name
+ base_name.foreign_key(false)
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
+ base_name.foreign_key
+ else
+ if ActiveRecord::Base != self && table_exists?
+ pk = connection.schema_cache.primary_keys(table_name)
+ suppress_composite_primary_key(pk)
+ else
+ "id"
+ end
+ end
+ end
+
+ # Sets the name of the primary key column.
+ #
+ # class Project < ActiveRecord::Base
+ # self.primary_key = 'sysid'
+ # end
+ #
+ # You can also define the #primary_key method yourself:
+ #
+ # class Project < ActiveRecord::Base
+ # def self.primary_key
+ # 'foo_' + super
+ # end
+ # end
+ #
+ # Project.primary_key # => "foo_id"
+ def primary_key=(value)
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
+ @attributes_builder = nil
+ end
+
+ private
+
+ def suppress_composite_primary_key(pk)
+ return pk unless pk.is_a?(Array)
+
+ warn <<~WARNING
+ WARNING: Active Record does not support composite primary key.
+
+ #{table_name} has composite primary key. Composite primary key is ignored.
+ WARNING
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
new file mode 100644
index 0000000000..6757e9b66a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ module Query
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "?"
+ end
+
+ def query_attribute(attr_name)
+ value = self[attr_name]
+
+ case value
+ when true then true
+ when false, nil then false
+ else
+ column = self.class.columns_hash[attr_name]
+ if column.nil?
+ if Numeric === value || value !~ /[^0-9]/
+ !value.to_i.zero?
+ else
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
+ !value.blank?
+ end
+ elsif value.respond_to?(:zero?)
+ !value.zero?
+ else
+ !value.blank?
+ end
+ end
+ end
+
+ private
+ # Handle *? for method_missing.
+ def attribute?(attribute_name)
+ query_attribute(attribute_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
new file mode 100644
index 0000000000..ffac5313ad
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ module Read
+ extend ActiveSupport::Concern
+
+ module ClassMethods # :nodoc:
+ private
+
+ def define_method_attribute(name)
+ sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
+
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}
+ #{sync_with_transaction_state}
+ name = #{attr_name_expr}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ RUBY
+ end
+ end
+ end
+
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
+ # to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name, &block)
+ name = attr_name.to_s
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
+
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id" && primary_key
+ sync_with_transaction_state if name == primary_key
+ _read_attribute(name, &block)
+ end
+
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the read_attribute API
+ def _read_attribute(attr_name, &block) # :nodoc
+ @attributes.fetch_value(attr_name.to_s, &block)
+ end
+
+ alias :attribute :_read_attribute
+ private :attribute
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
new file mode 100644
index 0000000000..6e0e90f39c
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ module Serialization
+ extend ActiveSupport::Concern
+
+ class ColumnNotSerializableError < StandardError
+ def initialize(name, type)
+ super <<~EOS
+ Column `#{name}` of type #{type.class} does not support `serialize` feature.
+ Usually it means that you are trying to use `serialize`
+ on a column that already implements serialization natively.
+ EOS
+ end
+ end
+
+ module ClassMethods
+ # If you have an attribute that needs to be saved to the database as an
+ # object, and retrieved as the same object, then specify the name of that
+ # attribute using this method and it will be handled automatically. The
+ # serialization is done through YAML. If +class_name+ is specified, the
+ # serialized object must be of that class on assignment and retrieval.
+ # Otherwise SerializationTypeMismatch will be raised.
+ #
+ # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of
+ # +Array+, will always be persisted as null.
+ #
+ # Keep in mind that database adapters handle certain serialization tasks
+ # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
+ # converted between JSON object/array syntax and Ruby +Hash+ or +Array+
+ # objects transparently. There is no need to use #serialize in this
+ # case.
+ #
+ # For more complex cases, such as conversion to or from your application
+ # domain objects, consider using the ActiveRecord::Attributes API.
+ #
+ # ==== Parameters
+ #
+ # * +attr_name+ - The field name that should be serialized.
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+
+ # or a class name that the object type should be equal to.
+ #
+ # ==== Example
+ #
+ # # Serialize a preferences attribute.
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ #
+ # # Serialize preferences using JSON as coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, JSON
+ # end
+ #
+ # # Serialize preferences as Hash using YAML coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, Hash
+ # end
+ def serialize(attr_name, class_name_or_coder = Object)
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
+ # using the #as_json hook.
+ coder = if class_name_or_coder == ::JSON
+ Coders::JSON
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ class_name_or_coder
+ else
+ Coders::YAMLColumn.new(attr_name, class_name_or_coder)
+ end
+
+ decorate_attribute_type(attr_name, :serialize) do |type|
+ if type_incompatible_with_serialize?(type, class_name_or_coder)
+ raise ColumnNotSerializableError.new(attr_name, type)
+ end
+
+ Type::Serialized.new(type, coder)
+ end
+ end
+
+ private
+
+ def type_incompatible_with_serialize?(type, class_name)
+ type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON ||
+ type.respond_to?(:type_cast_array, true) && class_name == ::Array
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
new file mode 100644
index 0000000000..294a3dc32c
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ module TimeZoneConversion
+ class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
+ def deserialize(value)
+ convert_time_to_time_zone(super)
+ end
+
+ def cast(value)
+ return if value.nil?
+
+ if value.is_a?(Hash)
+ set_time_zone_without_conversion(super)
+ elsif value.respond_to?(:in_time_zone)
+ begin
+ super(user_input_in_time_zone(value)) || super
+ rescue ArgumentError
+ nil
+ end
+ else
+ map_avoiding_infinite_recursion(super) { |v| cast(v) }
+ end
+ end
+
+ private
+
+ def convert_time_to_time_zone(value)
+ return if value.nil?
+
+ if value.acts_like?(:time)
+ value.in_time_zone
+ elsif value.is_a?(::Float)
+ value
+ else
+ map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) }
+ end
+ end
+
+ def set_time_zone_without_conversion(value)
+ ::Time.zone.local_to_utc(value).try(:in_time_zone) if value
+ end
+
+ def map_avoiding_infinite_recursion(value)
+ map(value) do |v|
+ if value.equal?(v)
+ nil
+ else
+ yield(v)
+ end
+ end
+ end
+ end
+
+ extend ActiveSupport::Concern
+
+ included do
+ mattr_accessor :time_zone_aware_attributes, instance_writer: false, default: false
+
+ class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: []
+ class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ]
+ end
+
+ module ClassMethods # :nodoc:
+ private
+
+ def inherited(subclass)
+ super
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type|
+ TimeZoneConverter.new(type)
+ end
+ end
+ end
+
+ def create_time_zone_conversion_attribute?(name, cast_type)
+ enabled_for_column = time_zone_aware_attributes &&
+ !skip_time_zone_conversion_for_attributes.include?(name.to_sym)
+
+ enabled_for_column && time_zone_aware_types.include?(cast_type.type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
new file mode 100644
index 0000000000..455e67e19b
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module AttributeMethods
+ module Write
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "="
+ end
+
+ module ClassMethods # :nodoc:
+ private
+
+ def define_method_attribute=(name)
+ sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
+
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name, writer: true,
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}(value)
+ name = #{attr_name_expr}
+ #{sync_with_transaction_state}
+ _write_attribute(name, value)
+ end
+ RUBY
+ end
+ end
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the
+ # specified +value+. Empty strings for Integer and Float columns are
+ # turned into +nil+.
+ def write_attribute(attr_name, value)
+ name = attr_name.to_s
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
+
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id" && primary_key
+ sync_with_transaction_state if name == primary_key
+ _write_attribute(name, value)
+ end
+
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the write_attribute API
+ def _write_attribute(attr_name, value) # :nodoc:
+ @attributes.write_from_user(attr_name.to_s, value)
+ value
+ end
+
+ private
+ def write_attribute_without_type_cast(attr_name, value)
+ name = attr_name.to_s
+ @attributes.write_cast_value(name, value)
+ value
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ _write_attribute(attribute_name, value)
+ end
+ end
+ end
+end