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.
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