dynamoid 3.3.0 → 3.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -1
- data/README.md +146 -52
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +20 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- 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 +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
- 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 +2 -1
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +10 -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 +68 -23
- data/lib/dynamoid/associations/single_association.rb +31 -4
- data/lib/dynamoid/components.rb +2 -0
- data/lib/dynamoid/config.rb +15 -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 +9 -1
- data/lib/dynamoid/criteria/chain.rb +421 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
- 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 +119 -64
- data/lib/dynamoid/document.rb +133 -46
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +251 -39
- data/lib/dynamoid/fields/declare.rb +86 -0
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +86 -17
- data/lib/dynamoid/loadable.rb +2 -2
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +502 -104
- data/lib/dynamoid/persistence/import.rb +2 -1
- data/lib/dynamoid/persistence/save.rb +1 -0
- data/lib/dynamoid/persistence/update_fields.rb +5 -2
- data/lib/dynamoid/persistence/update_validations.rb +18 -0
- data/lib/dynamoid/persistence/upsert.rb +5 -3
- 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 +6 -1
- data/lib/dynamoid/version.rb +1 -1
- metadata +48 -75
- 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 -44
- data/Appraisals +0 -22
- 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 -9
- data/gemfiles/rails_5_0.gemfile +0 -8
- data/gemfiles/rails_5_1.gemfile +0 -8
- data/gemfiles/rails_5_2.gemfile +0 -8
- data/gemfiles/rails_6_0.gemfile +0 -8
@@ -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,9 +1,9 @@
|
|
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
7
|
class Query
|
8
8
|
def initialize(query_hash)
|
9
9
|
@query_hash = query_hash
|
@@ -11,6 +11,10 @@ module Dynamoid #:nodoc:
|
|
11
11
|
@fields = query_hash.keys.map(&:to_s).map { |s| s.split('.').first }
|
12
12
|
end
|
13
13
|
|
14
|
+
def contain_only?(field_names)
|
15
|
+
(@fields - field_names.map(&:to_s)).blank?
|
16
|
+
end
|
17
|
+
|
14
18
|
def contain?(field_name)
|
15
19
|
@fields.include?(field_name.to_s)
|
16
20
|
end
|
@@ -20,13 +24,18 @@ module Dynamoid #:nodoc:
|
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
23
|
-
def initialize(query, source)
|
27
|
+
def initialize(query, source, forced_index_name: nil)
|
24
28
|
@query = query
|
25
29
|
@source = source
|
26
30
|
@query = Query.new(query)
|
31
|
+
@forced_index_name = forced_index_name
|
27
32
|
@result = find_keys_in_query
|
28
33
|
end
|
29
34
|
|
35
|
+
def non_key_present?
|
36
|
+
!@query.contain_only?([hash_key, range_key].compact)
|
37
|
+
end
|
38
|
+
|
30
39
|
def key_present?
|
31
40
|
@result.present?
|
32
41
|
end
|
@@ -46,6 +55,8 @@ module Dynamoid #:nodoc:
|
|
46
55
|
private
|
47
56
|
|
48
57
|
def find_keys_in_query
|
58
|
+
return match_forced_index if @forced_index_name
|
59
|
+
|
49
60
|
match_table_and_sort_key ||
|
50
61
|
match_local_secondary_index ||
|
51
62
|
match_global_secondary_index_and_sort_key ||
|
@@ -71,8 +82,8 @@ module Dynamoid #:nodoc:
|
|
71
82
|
def match_local_secondary_index
|
72
83
|
return unless @query.contain_with_eq_operator?(@source.hash_key)
|
73
84
|
|
74
|
-
lsi = @source.local_secondary_indexes.values.find do |
|
75
|
-
@query.contain?(
|
85
|
+
lsi = @source.local_secondary_indexes.values.find do |i|
|
86
|
+
@query.contain?(i.range_key)
|
76
87
|
end
|
77
88
|
|
78
89
|
if lsi.present?
|
@@ -90,9 +101,9 @@ module Dynamoid #:nodoc:
|
|
90
101
|
# But only do so if projects ALL attributes otherwise we won't
|
91
102
|
# get back full data
|
92
103
|
def match_global_secondary_index_and_sort_key
|
93
|
-
gsi = @source.global_secondary_indexes.values.find do |
|
94
|
-
@query.contain_with_eq_operator?(
|
95
|
-
@query.contain?(
|
104
|
+
gsi = @source.global_secondary_indexes.values.find do |i|
|
105
|
+
@query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all &&
|
106
|
+
@query.contain?(i.range_key)
|
96
107
|
end
|
97
108
|
|
98
109
|
if gsi.present?
|
@@ -113,8 +124,8 @@ module Dynamoid #:nodoc:
|
|
113
124
|
end
|
114
125
|
|
115
126
|
def match_global_secondary_index
|
116
|
-
gsi = @source.global_secondary_indexes.values.find do |
|
117
|
-
@query.contain_with_eq_operator?(
|
127
|
+
gsi = @source.global_secondary_indexes.values.find do |i|
|
128
|
+
@query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all
|
118
129
|
end
|
119
130
|
|
120
131
|
if gsi.present?
|
@@ -125,6 +136,16 @@ module Dynamoid #:nodoc:
|
|
125
136
|
}
|
126
137
|
end
|
127
138
|
end
|
139
|
+
|
140
|
+
def match_forced_index
|
141
|
+
idx = @source.find_index_by_name(@forced_index_name)
|
142
|
+
|
143
|
+
{
|
144
|
+
hash_key: idx.hash_key,
|
145
|
+
range_key: idx.range_key,
|
146
|
+
index_name: idx.name,
|
147
|
+
}
|
148
|
+
end
|
128
149
|
end
|
129
150
|
end
|
130
151
|
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
@@ -22,6 +22,7 @@ module Dynamoid
|
|
22
22
|
attribute_method_affix prefix: 'restore_', suffix: '!'
|
23
23
|
end
|
24
24
|
|
25
|
+
# @private
|
25
26
|
module ClassMethods
|
26
27
|
def update_fields(*)
|
27
28
|
if model = super
|
@@ -44,6 +45,7 @@ module Dynamoid
|
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
48
|
+
# @private
|
47
49
|
def save(*)
|
48
50
|
if status = super
|
49
51
|
changes_applied
|
@@ -51,24 +53,28 @@ module Dynamoid
|
|
51
53
|
status
|
52
54
|
end
|
53
55
|
|
56
|
+
# @private
|
54
57
|
def save!(*)
|
55
58
|
super.tap do
|
56
59
|
changes_applied
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
63
|
+
# @private
|
60
64
|
def update(*)
|
61
65
|
super.tap do
|
62
66
|
clear_changes_information
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
70
|
+
# @private
|
66
71
|
def update!(*)
|
67
72
|
super.tap do
|
68
73
|
clear_changes_information
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
77
|
+
# @private
|
72
78
|
def reload(*)
|
73
79
|
super.tap do
|
74
80
|
clear_changes_information
|
@@ -78,17 +84,22 @@ module Dynamoid
|
|
78
84
|
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
|
79
85
|
#
|
80
86
|
# person.changed? # => false
|
81
|
-
# person.name = '
|
87
|
+
# person.name = 'Bob'
|
82
88
|
# person.changed? # => true
|
89
|
+
#
|
90
|
+
# @return [true|false]
|
83
91
|
def changed?
|
84
92
|
changed_attributes.present?
|
85
93
|
end
|
86
94
|
|
87
|
-
# Returns an array with
|
95
|
+
# Returns an array with names of the attributes with unsaved changes.
|
88
96
|
#
|
97
|
+
# person = Person.new
|
89
98
|
# person.changed # => []
|
90
|
-
# person.name = '
|
99
|
+
# person.name = 'Bob'
|
91
100
|
# person.changed # => ["name"]
|
101
|
+
#
|
102
|
+
# @return [Array[String]]
|
92
103
|
def changed
|
93
104
|
changed_attributes.keys
|
94
105
|
end
|
@@ -97,18 +108,22 @@ module Dynamoid
|
|
97
108
|
# and new values like <tt>attr => [original value, new value]</tt>.
|
98
109
|
#
|
99
110
|
# person.changes # => {}
|
100
|
-
# person.name = '
|
101
|
-
# person.changes # => { "name" => ["
|
111
|
+
# person.name = 'Bob'
|
112
|
+
# person.changes # => { "name" => ["Bill", "Bob"] }
|
113
|
+
#
|
114
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
102
115
|
def changes
|
103
116
|
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
104
117
|
end
|
105
118
|
|
106
119
|
# Returns a hash of attributes that were changed before the model was saved.
|
107
120
|
#
|
108
|
-
# person.name # => "
|
109
|
-
# person.name = '
|
121
|
+
# person.name # => "Bob"
|
122
|
+
# person.name = 'Robert'
|
110
123
|
# person.save
|
111
|
-
# person.previous_changes # => {"name" => ["
|
124
|
+
# person.previous_changes # => {"name" => ["Bob", "Robert"]}
|
125
|
+
#
|
126
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
112
127
|
def previous_changes
|
113
128
|
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
|
114
129
|
end
|
@@ -116,15 +131,28 @@ module Dynamoid
|
|
116
131
|
# Returns a hash of the attributes with unsaved changes indicating their original
|
117
132
|
# values like <tt>attr => original value</tt>.
|
118
133
|
#
|
119
|
-
# person.name # => "
|
120
|
-
# person.name = '
|
121
|
-
# person.changed_attributes # => {"name" => "
|
134
|
+
# person.name # => "Bob"
|
135
|
+
# person.name = 'Robert'
|
136
|
+
# person.changed_attributes # => {"name" => "Bob"}
|
137
|
+
#
|
138
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
122
139
|
def changed_attributes
|
123
140
|
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
124
141
|
end
|
125
142
|
|
126
143
|
# Handle <tt>*_changed?</tt> for +method_missing+.
|
127
|
-
|
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 = {})
|
128
156
|
result = changes_include?(attr)
|
129
157
|
result &&= options[:to] == __send__(attr) if options.key?(:to)
|
130
158
|
result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
|
@@ -132,88 +160,115 @@ module Dynamoid
|
|
132
160
|
end
|
133
161
|
|
134
162
|
# Handle <tt>*_was</tt> for +method_missing+.
|
135
|
-
|
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)
|
136
171
|
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
137
172
|
end
|
138
173
|
|
139
174
|
# Restore all previous data of the provided attributes.
|
175
|
+
#
|
176
|
+
# @param attributes [Array[Symbol]] a list of attribute names
|
140
177
|
def restore_attributes(attributes = changed)
|
141
178
|
attributes.each { |attr| restore_attribute! attr }
|
142
179
|
end
|
143
180
|
|
144
181
|
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
|
145
|
-
|
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)
|
146
192
|
previous_changes_include?(attr)
|
147
193
|
end
|
148
194
|
|
149
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]
|
150
205
|
def attribute_previous_change(attr)
|
151
206
|
previous_changes[attr] if attribute_previously_changed?(attr)
|
152
207
|
end
|
153
208
|
|
154
209
|
private
|
155
210
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
# Removes current changes and makes them accessible through +previous_changes+.
|
162
|
-
def changes_applied # :doc:
|
163
|
-
@previously_changed = changes
|
164
|
-
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
165
|
-
end
|
211
|
+
def changes_include?(attr_name)
|
212
|
+
attributes_changed_by_setter.include?(attr_name)
|
213
|
+
end
|
214
|
+
alias attribute_changed_by_setter? changes_include?
|
166
215
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
172
221
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
177
227
|
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
181
232
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
rescue TypeError, NoMethodError
|
186
|
-
end
|
233
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
234
|
+
def attribute_will_change!(attr)
|
235
|
+
return if attribute_changed?(attr)
|
187
236
|
|
188
|
-
|
237
|
+
begin
|
238
|
+
value = __send__(attr)
|
239
|
+
value = value.duplicable? ? value.clone : value
|
240
|
+
rescue TypeError, NoMethodError
|
189
241
|
end
|
190
242
|
|
191
|
-
|
192
|
-
|
193
|
-
if attribute_changed?(attr)
|
194
|
-
__send__("#{attr}=", changed_attributes[attr])
|
195
|
-
clear_attribute_changes([attr])
|
196
|
-
end
|
197
|
-
end
|
243
|
+
set_attribute_was(attr, value)
|
244
|
+
end
|
198
245
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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])
|
203
251
|
end
|
252
|
+
end
|
204
253
|
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
208
259
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
end
|
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
|
213
263
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
218
273
|
end
|
219
274
|
end
|