dynamoid 3.3.0 → 3.7.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 +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
|