dynamoid 3.2.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -1
- data/README.md +580 -241
- data/lib/dynamoid.rb +2 -0
- data/lib/dynamoid/adapter.rb +15 -15
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +82 -102
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +108 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +29 -16
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +15 -6
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +15 -5
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +5 -3
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +4 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +65 -22
- data/lib/dynamoid/associations/single_association.rb +28 -1
- data/lib/dynamoid/components.rb +8 -3
- data/lib/dynamoid/config.rb +16 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +2 -1
- data/lib/dynamoid/criteria/chain.rb +418 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +109 -32
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +239 -32
- data/lib/dynamoid/document.rb +130 -251
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/fields.rb +246 -20
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +76 -17
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +592 -122
- data/lib/dynamoid/persistence/import.rb +73 -0
- data/lib/dynamoid/persistence/save.rb +64 -0
- data/lib/dynamoid/persistence/update_fields.rb +63 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +49 -71
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -41
- data/Appraisals +0 -28
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -11
- data/gemfiles/rails_5_0.gemfile +0 -10
- data/gemfiles/rails_5_1.gemfile +0 -10
- data/gemfiles/rails_5_2.gemfile +0 -10
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Dynamoid
|
4
4
|
module Criteria
|
5
|
+
# @private
|
5
6
|
class IgnoredConditionsDetector
|
6
7
|
def initialize(conditions)
|
7
8
|
@conditions = conditions
|
@@ -24,8 +25,8 @@ module Dynamoid
|
|
24
25
|
def ignored_keys
|
25
26
|
@conditions.keys
|
26
27
|
.group_by(&method(:key_to_field))
|
27
|
-
.select { |
|
28
|
-
.flat_map { |
|
28
|
+
.select { |_, ary| ary.size > 1 }
|
29
|
+
.flat_map { |_, ary| ary[0..-2] }
|
29
30
|
end
|
30
31
|
|
31
32
|
def key_to_field(key)
|
@@ -38,4 +39,3 @@ module Dynamoid
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
end
|
41
|
-
|
@@ -1,59 +1,136 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Dynamoid
|
3
|
+
module Dynamoid
|
4
4
|
module Criteria
|
5
|
+
# @private
|
5
6
|
class KeyFieldsDetector
|
6
|
-
|
7
|
+
class Query
|
8
|
+
def initialize(query_hash)
|
9
|
+
@query_hash = query_hash
|
10
|
+
@fields_with_operator = query_hash.keys.map(&:to_s)
|
11
|
+
@fields = query_hash.keys.map(&:to_s).map { |s| s.split('.').first }
|
12
|
+
end
|
13
|
+
|
14
|
+
def contain_only?(field_names)
|
15
|
+
(@fields - field_names.map(&:to_s)).blank?
|
16
|
+
end
|
17
|
+
|
18
|
+
def contain?(field_name)
|
19
|
+
@fields.include?(field_name.to_s)
|
20
|
+
end
|
21
|
+
|
22
|
+
def contain_with_eq_operator?(field_name)
|
23
|
+
@fields_with_operator.include?(field_name.to_s)
|
24
|
+
end
|
25
|
+
end
|
7
26
|
|
8
27
|
def initialize(query, source)
|
9
28
|
@query = query
|
10
29
|
@source = source
|
30
|
+
@query = Query.new(query)
|
31
|
+
@result = find_keys_in_query
|
32
|
+
end
|
11
33
|
|
12
|
-
|
34
|
+
def non_key_present?
|
35
|
+
!@query.contain_only?([hash_key, range_key].compact)
|
13
36
|
end
|
14
37
|
|
15
38
|
def key_present?
|
16
|
-
@
|
39
|
+
@result.present?
|
40
|
+
end
|
41
|
+
|
42
|
+
def hash_key
|
43
|
+
@result && @result[:hash_key]
|
44
|
+
end
|
45
|
+
|
46
|
+
def range_key
|
47
|
+
@result && @result[:range_key]
|
48
|
+
end
|
49
|
+
|
50
|
+
def index_name
|
51
|
+
@result && @result[:index_name]
|
17
52
|
end
|
18
53
|
|
19
54
|
private
|
20
55
|
|
21
|
-
def
|
22
|
-
|
56
|
+
def find_keys_in_query
|
57
|
+
match_table_and_sort_key ||
|
58
|
+
match_local_secondary_index ||
|
59
|
+
match_global_secondary_index_and_sort_key ||
|
60
|
+
match_table ||
|
61
|
+
match_global_secondary_index
|
62
|
+
end
|
63
|
+
|
64
|
+
# Use table's default range key
|
65
|
+
def match_table_and_sort_key
|
66
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
67
|
+
return unless @source.range_key
|
23
68
|
|
24
|
-
|
25
|
-
|
26
|
-
|
69
|
+
if @query.contain?(@source.range_key)
|
70
|
+
{
|
71
|
+
hash_key: @source.hash_key,
|
72
|
+
range_key: @source.range_key
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
27
76
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
77
|
+
# See if can use any local secondary index range key
|
78
|
+
# Chooses the first LSI found that can be utilized for the query
|
79
|
+
def match_local_secondary_index
|
80
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
33
81
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
82
|
+
lsi = @source.local_secondary_indexes.values.find do |i|
|
83
|
+
@query.contain?(i.range_key)
|
84
|
+
end
|
85
|
+
|
86
|
+
if lsi.present?
|
87
|
+
{
|
88
|
+
hash_key: @source.hash_key,
|
89
|
+
range_key: lsi.range_key,
|
90
|
+
index_name: lsi.name,
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
38
94
|
|
39
|
-
|
40
|
-
|
41
|
-
|
95
|
+
# See if can use any global secondary index
|
96
|
+
# Chooses the first GSI found that can be utilized for the query
|
97
|
+
# GSI with range key involved into query conditions has higher priority
|
98
|
+
# But only do so if projects ALL attributes otherwise we won't
|
99
|
+
# get back full data
|
100
|
+
def match_global_secondary_index_and_sort_key
|
101
|
+
gsi = @source.global_secondary_indexes.values.find do |i|
|
102
|
+
@query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all &&
|
103
|
+
@query.contain?(i.range_key)
|
104
|
+
end
|
42
105
|
|
43
|
-
|
106
|
+
if gsi.present?
|
107
|
+
{
|
108
|
+
hash_key: gsi.hash_key,
|
109
|
+
range_key: gsi.range_key,
|
110
|
+
index_name: gsi.name,
|
111
|
+
}
|
44
112
|
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def match_table
|
116
|
+
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
45
117
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
118
|
+
{
|
119
|
+
hash_key: @source.hash_key,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def match_global_secondary_index
|
124
|
+
gsi = @source.global_secondary_indexes.values.find do |i|
|
125
|
+
@query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all
|
126
|
+
end
|
53
127
|
|
54
|
-
|
55
|
-
|
56
|
-
|
128
|
+
if gsi.present?
|
129
|
+
{
|
130
|
+
hash_key: gsi.hash_key,
|
131
|
+
range_key: gsi.range_key,
|
132
|
+
index_name: gsi.name,
|
133
|
+
}
|
57
134
|
end
|
58
135
|
end
|
59
136
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Dynamoid
|
4
4
|
module Criteria
|
5
|
+
# @private
|
5
6
|
class NonexistentFieldsDetector
|
6
7
|
def initialize(conditions, source)
|
7
8
|
@conditions = conditions
|
@@ -19,8 +20,8 @@ module Dynamoid
|
|
19
20
|
fields_list = @nonexistent_fields.map { |s| "`#{s}`" }.join(', ')
|
20
21
|
count = @nonexistent_fields.size
|
21
22
|
|
22
|
-
|
23
|
-
" field #{
|
23
|
+
'where conditions contain nonexistent' \
|
24
|
+
" field #{'name'.pluralize(count)} #{fields_list}"
|
24
25
|
end
|
25
26
|
|
26
27
|
private
|
data/lib/dynamoid/dirty.rb
CHANGED
@@ -1,67 +1,274 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Dynamoid
|
4
|
+
# Support interface of Rails' ActiveModel::Dirty module
|
5
|
+
#
|
6
|
+
# The reason why not just include ActiveModel::Dirty -
|
7
|
+
# ActiveModel::Dirty conflicts either with @attributes or
|
8
|
+
# #attributes in different Rails versions.
|
9
|
+
#
|
10
|
+
# Separate implementation (or copy-pasting) is the best way to
|
11
|
+
# avoid endless monkey-patching
|
12
|
+
#
|
13
|
+
# Documentation:
|
14
|
+
# https://api.rubyonrails.org/v4.2/classes/ActiveModel/Dirty.html
|
4
15
|
module Dirty
|
5
16
|
extend ActiveSupport::Concern
|
6
|
-
include ActiveModel::
|
17
|
+
include ActiveModel::AttributeMethods
|
7
18
|
|
19
|
+
included do
|
20
|
+
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
|
21
|
+
attribute_method_suffix '_previously_changed?', '_previous_change'
|
22
|
+
attribute_method_affix prefix: 'restore_', suffix: '!'
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
8
26
|
module ClassMethods
|
27
|
+
def update_fields(*)
|
28
|
+
if model = super
|
29
|
+
model.send(:clear_changes_information)
|
30
|
+
end
|
31
|
+
model
|
32
|
+
end
|
33
|
+
|
34
|
+
def upsert(*)
|
35
|
+
if model = super
|
36
|
+
model.send(:clear_changes_information)
|
37
|
+
end
|
38
|
+
model
|
39
|
+
end
|
40
|
+
|
9
41
|
def from_database(*)
|
10
|
-
super.tap
|
42
|
+
super.tap do |m|
|
43
|
+
m.send(:clear_changes_information)
|
44
|
+
end
|
11
45
|
end
|
12
46
|
end
|
13
47
|
|
48
|
+
# @private
|
14
49
|
def save(*)
|
15
|
-
|
50
|
+
if status = super
|
51
|
+
changes_applied
|
52
|
+
end
|
53
|
+
status
|
16
54
|
end
|
17
55
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
56
|
+
# @private
|
57
|
+
def save!(*)
|
58
|
+
super.tap do
|
59
|
+
changes_applied
|
60
|
+
end
|
22
61
|
end
|
23
62
|
|
24
|
-
|
25
|
-
|
63
|
+
# @private
|
64
|
+
def update(*)
|
65
|
+
super.tap do
|
66
|
+
clear_changes_information
|
67
|
+
end
|
26
68
|
end
|
27
69
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
70
|
+
# @private
|
71
|
+
def update!(*)
|
72
|
+
super.tap do
|
73
|
+
clear_changes_information
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @private
|
78
|
+
def reload(*)
|
79
|
+
super.tap do
|
80
|
+
clear_changes_information
|
35
81
|
end
|
36
82
|
end
|
37
83
|
|
38
|
-
|
39
|
-
|
40
|
-
|
84
|
+
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
|
85
|
+
#
|
86
|
+
# person.changed? # => false
|
87
|
+
# person.name = 'Bob'
|
88
|
+
# person.changed? # => true
|
89
|
+
#
|
90
|
+
# @return [true|false]
|
91
|
+
def changed?
|
92
|
+
changed_attributes.present?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns an array with names of the attributes with unsaved changes.
|
96
|
+
#
|
97
|
+
# person = Person.new
|
98
|
+
# person.changed # => []
|
99
|
+
# person.name = 'Bob'
|
100
|
+
# person.changed # => ["name"]
|
101
|
+
#
|
102
|
+
# @return [Array[String]]
|
103
|
+
def changed
|
104
|
+
changed_attributes.keys
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a hash of changed attributes indicating their original
|
108
|
+
# and new values like <tt>attr => [original value, new value]</tt>.
|
109
|
+
#
|
110
|
+
# person.changes # => {}
|
111
|
+
# person.name = 'Bob'
|
112
|
+
# person.changes # => { "name" => ["Bill", "Bob"] }
|
113
|
+
#
|
114
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
115
|
+
def changes
|
116
|
+
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
41
117
|
end
|
42
118
|
|
43
|
-
|
119
|
+
# Returns a hash of attributes that were changed before the model was saved.
|
120
|
+
#
|
121
|
+
# person.name # => "Bob"
|
122
|
+
# person.name = 'Robert'
|
123
|
+
# person.save
|
124
|
+
# person.previous_changes # => {"name" => ["Bob", "Robert"]}
|
125
|
+
#
|
126
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
127
|
+
def previous_changes
|
128
|
+
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
|
129
|
+
end
|
44
130
|
|
45
|
-
|
46
|
-
|
131
|
+
# Returns a hash of the attributes with unsaved changes indicating their original
|
132
|
+
# values like <tt>attr => original value</tt>.
|
133
|
+
#
|
134
|
+
# person.name # => "Bob"
|
135
|
+
# person.name = 'Robert'
|
136
|
+
# person.changed_attributes # => {"name" => "Bob"}
|
137
|
+
#
|
138
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
139
|
+
def changed_attributes
|
140
|
+
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
47
141
|
end
|
48
142
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
143
|
+
# Handle <tt>*_changed?</tt> for +method_missing+.
|
144
|
+
#
|
145
|
+
# person.attribute_changed?(:name) # => true
|
146
|
+
# person.attribute_changed?(:name, from: 'Alice')
|
147
|
+
# person.attribute_changed?(:name, to: 'Bob')
|
148
|
+
# person.attribute_changed?(:name, from: 'Alice', to: 'Bod')
|
149
|
+
#
|
150
|
+
# @private
|
151
|
+
# @param attr [Symbol] attribute name
|
152
|
+
# @param options [Hash] conditions on +from+ and +to+ value (optional)
|
153
|
+
# @option options [Symbol] :from previous attribute value
|
154
|
+
# @option options [Symbol] :to current attribute value
|
155
|
+
def attribute_changed?(attr, options = {})
|
156
|
+
result = changes_include?(attr)
|
157
|
+
result &&= options[:to] == __send__(attr) if options.key?(:to)
|
158
|
+
result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
|
159
|
+
result
|
160
|
+
end
|
53
161
|
|
54
|
-
|
55
|
-
|
162
|
+
# Handle <tt>*_was</tt> for +method_missing+.
|
163
|
+
#
|
164
|
+
# person = Person.create(name: 'Alice')
|
165
|
+
# person.name = 'Bob'
|
166
|
+
# person.attribute_was(:name) # => "Alice"
|
167
|
+
#
|
168
|
+
# @private
|
169
|
+
# @param attr [Symbol] attribute name
|
170
|
+
def attribute_was(attr)
|
171
|
+
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Restore all previous data of the provided attributes.
|
175
|
+
#
|
176
|
+
# @param attributes [Array[Symbol]] a list of attribute names
|
177
|
+
def restore_attributes(attributes = changed)
|
178
|
+
attributes.each { |attr| restore_attribute! attr }
|
179
|
+
end
|
180
|
+
|
181
|
+
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
|
182
|
+
#
|
183
|
+
# person = Person.create(name: 'Alice')
|
184
|
+
# person.name = 'Bob'
|
185
|
+
# person.save
|
186
|
+
# person.attribute_changed?(:name) # => true
|
187
|
+
#
|
188
|
+
# @private
|
189
|
+
# @param attr [Symbol] attribute name
|
190
|
+
# @return [true|false]
|
191
|
+
def attribute_previously_changed?(attr)
|
192
|
+
previous_changes_include?(attr)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Handles <tt>*_previous_change</tt> for +method_missing+.
|
196
|
+
#
|
197
|
+
# person = Person.create(name: 'Alice')
|
198
|
+
# person.name = 'Bob'
|
199
|
+
# person.save
|
200
|
+
# person.attribute_previously_changed(:name) # => ["Alice", "Bob"]
|
201
|
+
#
|
202
|
+
# @private
|
203
|
+
# @param attr [Symbol]
|
204
|
+
# @return [Array]
|
205
|
+
def attribute_previous_change(attr)
|
206
|
+
previous_changes[attr] if attribute_previously_changed?(attr)
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def changes_include?(attr_name)
|
212
|
+
attributes_changed_by_setter.include?(attr_name)
|
213
|
+
end
|
214
|
+
alias attribute_changed_by_setter? changes_include?
|
215
|
+
|
216
|
+
# Removes current changes and makes them accessible through +previous_changes+.
|
217
|
+
def changes_applied # :doc:
|
218
|
+
@previously_changed = changes
|
219
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
220
|
+
end
|
221
|
+
|
222
|
+
# Clear all dirty data: current changes and previous changes.
|
223
|
+
def clear_changes_information # :doc:
|
224
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
225
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
226
|
+
end
|
227
|
+
|
228
|
+
# Handle <tt>*_change</tt> for +method_missing+.
|
229
|
+
def attribute_change(attr)
|
230
|
+
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
234
|
+
def attribute_will_change!(attr)
|
235
|
+
return if attribute_changed?(attr)
|
236
|
+
|
237
|
+
begin
|
238
|
+
value = __send__(attr)
|
239
|
+
value = value.duplicable? ? value.clone : value
|
240
|
+
rescue TypeError, NoMethodError
|
56
241
|
end
|
57
242
|
|
58
|
-
|
243
|
+
set_attribute_was(attr, value)
|
59
244
|
end
|
60
245
|
|
61
|
-
|
62
|
-
|
63
|
-
|
246
|
+
# Handle <tt>restore_*!</tt> for +method_missing+.
|
247
|
+
def restore_attribute!(attr)
|
248
|
+
if attribute_changed?(attr)
|
249
|
+
__send__("#{attr}=", changed_attributes[attr])
|
250
|
+
clear_attribute_changes([attr])
|
64
251
|
end
|
65
252
|
end
|
253
|
+
|
254
|
+
# Returns +true+ if attr_name were changed before the model was saved,
|
255
|
+
# +false+ otherwise.
|
256
|
+
def previous_changes_include?(attr_name)
|
257
|
+
previous_changes.include?(attr_name)
|
258
|
+
end
|
259
|
+
|
260
|
+
# This is necessary because `changed_attributes` might be overridden in
|
261
|
+
# other implemntations (e.g. in `ActiveRecord`)
|
262
|
+
alias attributes_changed_by_setter changed_attributes
|
263
|
+
|
264
|
+
# Force an attribute to have a particular "before" value
|
265
|
+
def set_attribute_was(attr, old_value)
|
266
|
+
attributes_changed_by_setter[attr] = old_value
|
267
|
+
end
|
268
|
+
|
269
|
+
# Remove changes information for the provided attributes.
|
270
|
+
def clear_attribute_changes(attributes)
|
271
|
+
attributes_changed_by_setter.except!(*attributes)
|
272
|
+
end
|
66
273
|
end
|
67
274
|
end
|