dynamoid 3.3.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +146 -52
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +20 -7
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
  17. data/lib/dynamoid/application_time_zone.rb +1 -0
  18. data/lib/dynamoid/associations.rb +182 -19
  19. data/lib/dynamoid/associations/association.rb +10 -2
  20. data/lib/dynamoid/associations/belongs_to.rb +2 -1
  21. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
  22. data/lib/dynamoid/associations/has_many.rb +2 -1
  23. data/lib/dynamoid/associations/has_one.rb +2 -1
  24. data/lib/dynamoid/associations/many_association.rb +68 -23
  25. data/lib/dynamoid/associations/single_association.rb +31 -4
  26. data/lib/dynamoid/components.rb +2 -0
  27. data/lib/dynamoid/config.rb +15 -3
  28. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
  29. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
  30. data/lib/dynamoid/config/options.rb +1 -0
  31. data/lib/dynamoid/criteria.rb +9 -1
  32. data/lib/dynamoid/criteria/chain.rb +421 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
  37. data/lib/dynamoid/dirty.rb +119 -64
  38. data/lib/dynamoid/document.rb +133 -46
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/errors.rb +2 -0
  42. data/lib/dynamoid/fields.rb +251 -39
  43. data/lib/dynamoid/fields/declare.rb +86 -0
  44. data/lib/dynamoid/finders.rb +69 -32
  45. data/lib/dynamoid/identity_map.rb +6 -0
  46. data/lib/dynamoid/indexes.rb +86 -17
  47. data/lib/dynamoid/loadable.rb +2 -2
  48. data/lib/dynamoid/log/formatter.rb +26 -0
  49. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  50. data/lib/dynamoid/persistence.rb +502 -104
  51. data/lib/dynamoid/persistence/import.rb +2 -1
  52. data/lib/dynamoid/persistence/save.rb +1 -0
  53. data/lib/dynamoid/persistence/update_fields.rb +5 -2
  54. data/lib/dynamoid/persistence/update_validations.rb +18 -0
  55. data/lib/dynamoid/persistence/upsert.rb +5 -3
  56. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  57. data/lib/dynamoid/railtie.rb +1 -0
  58. data/lib/dynamoid/tasks.rb +3 -1
  59. data/lib/dynamoid/tasks/database.rb +1 -0
  60. data/lib/dynamoid/type_casting.rb +12 -2
  61. data/lib/dynamoid/undumping.rb +8 -0
  62. data/lib/dynamoid/validations.rb +6 -1
  63. data/lib/dynamoid/version.rb +1 -1
  64. metadata +48 -75
  65. data/.coveralls.yml +0 -1
  66. data/.document +0 -5
  67. data/.gitignore +0 -74
  68. data/.rspec +0 -2
  69. data/.rubocop.yml +0 -71
  70. data/.rubocop_todo.yml +0 -55
  71. data/.travis.yml +0 -44
  72. data/Appraisals +0 -22
  73. data/Gemfile +0 -8
  74. data/Rakefile +0 -46
  75. data/Vagrantfile +0 -29
  76. data/docker-compose.yml +0 -7
  77. data/dynamoid.gemspec +0 -57
  78. data/gemfiles/rails_4_2.gemfile +0 -9
  79. data/gemfiles/rails_5_0.gemfile +0 -8
  80. data/gemfiles/rails_5_1.gemfile +0 -8
  81. data/gemfiles/rails_5_2.gemfile +0 -8
  82. 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 { |field, ary| ary.size > 1 }
28
- .flat_map { |field, ary| ary[0 .. -2] }
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 #:nodoc:
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 |lsi|
75
- @query.contain?(lsi.range_key)
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 |gsi|
94
- @query.contain_with_eq_operator?(gsi.hash_key) && gsi.projected_attributes == :all &&
95
- @query.contain?(gsi.range_key)
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 |gsi|
117
- @query.contain_with_eq_operator?(gsi.hash_key) && gsi.projected_attributes == :all
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
- "where conditions contain nonexistent" \
23
- " field #{ 'name'.pluralize(count) } #{ fields_list }"
23
+ 'where conditions contain nonexistent' \
24
+ " field #{'name'.pluralize(count)} #{fields_list}"
24
25
  end
25
26
 
26
27
  private
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Dynamoid
4
4
  module Criteria
5
+ # @private
5
6
  class OverwrittenConditionsDetector
6
7
  def initialize(conditions, conditions_new)
7
8
  @conditions = conditions
@@ -37,4 +38,3 @@ module Dynamoid
37
38
  end
38
39
  end
39
40
  end
40
-
@@ -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 = 'bob'
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 the name of the attributes with unsaved changes.
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 = 'bob'
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 = 'bob'
101
- # person.changes # => { "name" => ["bill", "bob"] }
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 # => "bob"
109
- # person.name = 'robert'
121
+ # person.name # => "Bob"
122
+ # person.name = 'Robert'
110
123
  # person.save
111
- # person.previous_changes # => {"name" => ["bob", "robert"]}
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 # => "bob"
120
- # person.name = 'robert'
121
- # person.changed_attributes # => {"name" => "bob"}
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
- def attribute_changed?(attr, options = {}) #:nodoc:
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
- def attribute_was(attr) # :nodoc:
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
- def attribute_previously_changed?(attr) #:nodoc:
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
- def changes_include?(attr_name)
157
- attributes_changed_by_setter.include?(attr_name)
158
- end
159
- alias attribute_changed_by_setter? changes_include?
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
- # Clear all dirty data: current changes and previous changes.
168
- def clear_changes_information # :doc:
169
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
170
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
171
- end
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
- # Handle <tt>*_change</tt> for +method_missing+.
174
- def attribute_change(attr)
175
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
176
- end
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
- # Handle <tt>*_will_change!</tt> for +method_missing+.
179
- def attribute_will_change!(attr)
180
- return if attribute_changed?(attr)
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
- begin
183
- value = __send__(attr)
184
- value = value.duplicable? ? value.clone : value
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
- set_attribute_was(attr, value)
237
+ begin
238
+ value = __send__(attr)
239
+ value = value.duplicable? ? value.clone : value
240
+ rescue TypeError, NoMethodError
189
241
  end
190
242
 
191
- # Handle <tt>restore_*!</tt> for +method_missing+.
192
- def restore_attribute!(attr)
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
- # Returns +true+ if attr_name were changed before the model was saved,
200
- # +false+ otherwise.
201
- def previous_changes_include?(attr_name)
202
- previous_changes.include?(attr_name)
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
- # This is necessary because `changed_attributes` might be overridden in
206
- # other implemntations (e.g. in `ActiveRecord`)
207
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
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
- # Force an attribute to have a particular "before" value
210
- def set_attribute_was(attr, old_value)
211
- attributes_changed_by_setter[attr] = old_value
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
- # Remove changes information for the provided attributes.
215
- def clear_attribute_changes(attributes) # :doc:
216
- attributes_changed_by_setter.except!(*attributes)
217
- end
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