aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/attribute_methods/dirty.rb
blob: 6a5c057384fed4789523270cd4cc9e297edfbaa1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
require 'active_support/core_ext/module/attribute_accessors'

module ActiveRecord
  module AttributeMethods
    module Dirty # :nodoc:
      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
        self.partial_writes = true
      end

      # Attempts to +save+ the record and clears changed attributes if successful.
      def save(*)
        if status = super
          changes_applied
        end
        status
      end

      # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
      def save!(*)
        super.tap do
          changes_applied
        end
      end

      # <tt>reload</tt> the record and clears changed attributes.
      def reload(*)
        super.tap do
          reset_changes
        end
      end

      def initialize_dup(other) # :nodoc:
        super
        init_changed_attributes
      end

      def changed?
        super || changed_in_place.any?
      end

      def changed
        super | changed_in_place
      end

      def attribute_changed?(attr_name, options = {})
        result = super
        # We can't change "from" something in place. Only setters can define
        # "from" and "to"
        result ||= changed_in_place?(attr_name) unless options.key?(:from)
        result
      end

      def changes_applied
        super
        store_original_raw_attributes
      end

      def reset_changes
        super
        original_raw_attributes.clear
      end

      private

      def initialize_internals_callback
        super
        init_changed_attributes
      end

      def init_changed_attributes
        @changed_attributes = nil
        # Intentionally avoid using #column_defaults since overridden defaults (as is done in
        # optimistic locking) won't get written unless they get marked as changed
        self.class.columns.each do |c|
          attr, orig_value = c.name, c.default
          changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
        end
      end

      # Wrap write_attribute to remember original attribute value.
      def write_attribute(attr, value)
        attr = attr.to_s

        old_value = old_attribute_value(attr)

        result = super
        store_original_raw_attribute(attr)
        save_changed_attribute(attr, old_value)
        result
      end

      def raw_write_attribute(attr, value)
        attr = attr.to_s

        result = super
        original_raw_attributes[attr] = value
        result
      end

      def save_changed_attribute(attr, old_value)
        if attribute_changed?(attr)
          changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
        else
          changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
        end
      end

      def old_attribute_value(attr)
        if attribute_changed?(attr)
          changed_attributes[attr]
        else
          clone_attribute_value(:read_attribute, attr)
        end
      end

      def _update_record(*)
        partial_writes? ? super(keys_for_partial_write) : super
      end

      def _create_record(*)
        partial_writes? ? super(keys_for_partial_write) : super
      end

      # Serialized attributes should always be written in case they've been
      # changed in place.
      def keys_for_partial_write
        changed
      end

      def _field_changed?(attr, old_value)
        new_value = read_attribute(attr)
        raw_value = read_attribute_before_type_cast(attr)
        column_for_attribute(attr).changed?(old_value, new_value, raw_value)
      end

      def changed_in_place
        self.class.attribute_names.select do |attr_name|
          changed_in_place?(attr_name)
        end
      end

      def changed_in_place?(attr_name)
        type = type_for_attribute(attr_name)
        old_value = original_raw_attribute(attr_name)
        value = read_attribute(attr_name)
        type.changed_in_place?(old_value, value)
      end

      def original_raw_attribute(attr_name)
        original_raw_attributes.fetch(attr_name) do
          read_attribute_before_type_cast(attr_name)
        end
      end

      def original_raw_attributes
        @original_raw_attributes ||= {}
      end

      def store_original_raw_attribute(attr_name)
        type = type_for_attribute(attr_name)
        value = type.type_cast_for_database(read_attribute(attr_name))
        original_raw_attributes[attr_name] = value
      end

      def store_original_raw_attributes
        attribute_names.each do |attr|
          store_original_raw_attribute(attr)
        end
      end
    end
  end
end