dynamoid 1.3.4 → 2.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +37 -7
  5. data/Appraisals +11 -0
  6. data/CHANGELOG.md +115 -2
  7. data/Gemfile +2 -0
  8. data/LICENSE.txt +18 -16
  9. data/README.md +253 -34
  10. data/Rakefile +0 -24
  11. data/Vagrantfile +1 -1
  12. data/docker-compose.yml +7 -0
  13. data/dynamoid.gemspec +4 -4
  14. data/gemfiles/rails_4_0.gemfile +3 -3
  15. data/gemfiles/rails_4_1.gemfile +3 -3
  16. data/gemfiles/rails_4_2.gemfile +3 -3
  17. data/gemfiles/rails_5_0.gemfile +2 -1
  18. data/gemfiles/rails_5_1.gemfile +8 -0
  19. data/gemfiles/rails_5_2.gemfile +8 -0
  20. data/lib/dynamoid.rb +31 -31
  21. data/lib/dynamoid/adapter.rb +14 -10
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +188 -100
  23. data/lib/dynamoid/associations.rb +21 -12
  24. data/lib/dynamoid/associations/association.rb +19 -3
  25. data/lib/dynamoid/associations/belongs_to.rb +26 -16
  26. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -16
  27. data/lib/dynamoid/associations/has_many.rb +2 -17
  28. data/lib/dynamoid/associations/has_one.rb +0 -14
  29. data/lib/dynamoid/associations/many_association.rb +19 -6
  30. data/lib/dynamoid/associations/single_association.rb +25 -7
  31. data/lib/dynamoid/config.rb +37 -18
  32. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +11 -0
  33. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +25 -0
  34. data/lib/dynamoid/config/options.rb +1 -1
  35. data/lib/dynamoid/criteria/chain.rb +48 -32
  36. data/lib/dynamoid/dirty.rb +23 -4
  37. data/lib/dynamoid/document.rb +88 -5
  38. data/lib/dynamoid/errors.rb +4 -1
  39. data/lib/dynamoid/fields.rb +6 -6
  40. data/lib/dynamoid/finders.rb +42 -12
  41. data/lib/dynamoid/identity_map.rb +0 -1
  42. data/lib/dynamoid/indexes.rb +41 -54
  43. data/lib/dynamoid/persistence.rb +151 -40
  44. data/lib/dynamoid/railtie.rb +1 -1
  45. data/lib/dynamoid/validations.rb +4 -3
  46. data/lib/dynamoid/version.rb +1 -1
  47. metadata +18 -29
  48. data/gemfiles/rails_4_0.gemfile.lock +0 -150
  49. data/gemfiles/rails_4_1.gemfile.lock +0 -154
  50. data/gemfiles/rails_4_2.gemfile.lock +0 -175
  51. data/gemfiles/rails_5_0.gemfile.lock +0 -180
@@ -0,0 +1,11 @@
1
+ module Dynamoid
2
+ module Config
3
+ module BackoffStrategies
4
+ class ConstantBackoff
5
+ def self.call(n = 1)
6
+ -> { sleep n }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Dynamoid
2
+ module Config
3
+ module BackoffStrategies
4
+ # Truncated binary exponential backoff algorithm
5
+ # See https://en.wikipedia.org/wiki/Exponential_backoff
6
+ class ExponentialBackoff
7
+ def self.call(opts = {})
8
+ opts = { base_backoff: 0.5, ceiling: 3 }.merge(opts)
9
+ base_backoff = opts[:base_backoff]
10
+ ceiling = opts[:ceiling]
11
+
12
+ times = 1
13
+
14
+ lambda do
15
+ power = [times - 1, ceiling - 1].min
16
+ backoff = base_backoff * (2 ** power)
17
+ sleep backoff
18
+
19
+ times += 1
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -43,7 +43,7 @@ module Dynamoid #:nodoc
43
43
  def #{name}?
44
44
  #{name}
45
45
  end
46
-
46
+
47
47
  def reset_#{name}
48
48
  settings[#{name.inspect}] = defaults[#{name.inspect}]
49
49
  end
@@ -18,6 +18,11 @@ module Dynamoid #:nodoc:
18
18
  @source = source
19
19
  @consistent_read = false
20
20
  @scan_index_forward = true
21
+
22
+ # Honor STI and :type field if it presents
23
+ if @source.attributes.key?(:type)
24
+ @query[:'type.in'] = @source.deep_subclasses.map(&:name) << @source.name
25
+ end
21
26
  end
22
27
 
23
28
  # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
@@ -56,32 +61,36 @@ module Dynamoid #:nodoc:
56
61
  end
57
62
 
58
63
  # Returns the last fetched record matched the criteria
64
+ # Enumerable doesn't implement `last`, only `first`
65
+ # So we have to implement it ourselves
59
66
  #
60
67
  def last
61
- all.last
68
+ all.to_a.last
62
69
  end
63
70
 
64
71
  # Destroys all the records matching the criteria.
65
72
  #
66
- def destroy_all
73
+ def delete_all
67
74
  ids = []
75
+ ranges = []
68
76
 
69
77
  if key_present?
70
- ranges = []
71
78
  Dynamoid.adapter.query(source.table_name, range_query).collect do |hash|
72
79
  ids << hash[source.hash_key.to_sym]
73
- ranges << hash[source.range_key.to_sym]
80
+ ranges << hash[source.range_key.to_sym] if source.range_key
74
81
  end
75
82
 
76
- Dynamoid.adapter.delete(source.table_name, ids,{:range_key => ranges})
83
+ Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence)
77
84
  else
78
- Dynamoid.adapter.scan(source.table_name, query, scan_opts).collect do |hash|
85
+ Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).collect do |hash|
79
86
  ids << hash[source.hash_key.to_sym]
87
+ ranges << hash[source.range_key.to_sym] if source.range_key
80
88
  end
81
89
 
82
- Dynamoid.adapter.delete(source.table_name, ids)
90
+ Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence)
83
91
  end
84
92
  end
93
+ alias_method :destroy_all, :delete_all
85
94
 
86
95
  # The record limit is the limit of evaluated records returned by the
87
96
  # query or scan.
@@ -121,10 +130,6 @@ module Dynamoid #:nodoc:
121
130
  records.each(&block)
122
131
  end
123
132
 
124
- def consistent_opts
125
- { :consistent_read => consistent_read }
126
- end
127
-
128
133
  private
129
134
 
130
135
  # The actual records referenced by the association.
@@ -133,12 +138,11 @@ module Dynamoid #:nodoc:
133
138
  #
134
139
  # @since 0.2.0
135
140
  def records
136
- results = if key_present?
141
+ if key_present?
137
142
  records_via_query
138
143
  else
139
144
  records_via_scan
140
145
  end
141
- @batch_size ? results : Array(results)
142
146
  end
143
147
 
144
148
  def records_via_query
@@ -157,7 +161,10 @@ module Dynamoid #:nodoc:
157
161
  def records_via_scan
158
162
  if Dynamoid::Config.warn_on_scan
159
163
  Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
160
- Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}]"
164
+ Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:"
165
+ Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
166
+ Dynamoid.logger.warn "* local_secondary_indexe range_key: 'some-name'"
167
+ Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}"
161
168
  end
162
169
 
163
170
  Enumerator.new do |yielder|
@@ -173,17 +180,17 @@ module Dynamoid #:nodoc:
173
180
 
174
181
  case operation
175
182
  when 'gt'
176
- { :range_greater_than => val }
183
+ { range_greater_than: val }
177
184
  when 'lt'
178
- { :range_less_than => val }
185
+ { range_less_than: val }
179
186
  when 'gte'
180
- { :range_gte => val }
187
+ { range_gte: val }
181
188
  when 'lte'
182
- { :range_lte => val }
189
+ { range_lte: val }
183
190
  when 'between'
184
- { :range_between => val }
191
+ { range_between: val }
185
192
  when 'begins_with'
186
- { :range_begins_with => val }
193
+ { range_begins_with: val }
187
194
  end
188
195
  end
189
196
 
@@ -192,6 +199,8 @@ module Dynamoid #:nodoc:
192
199
  val = type_cast_condition_parameter(name, query[key])
193
200
 
194
201
  hash = case operation
202
+ when 'ne'
203
+ { ne: val }
195
204
  when 'gt'
196
205
  { gt: val }
197
206
  when 'lt'
@@ -215,6 +224,10 @@ module Dynamoid #:nodoc:
215
224
  return { name.to_sym => hash }
216
225
  end
217
226
 
227
+ def consistent_opts
228
+ { consistent_read: consistent_read }
229
+ end
230
+
218
231
  def range_query
219
232
  opts = {}
220
233
 
@@ -227,7 +240,7 @@ module Dynamoid #:nodoc:
227
240
  opts[:range_key] = @range_key
228
241
  if query[@range_key].present?
229
242
  value = type_cast_condition_parameter(@range_key, query[@range_key])
230
- opts.update(:range_eq => value)
243
+ opts.update(range_eq: value)
231
244
  end
232
245
 
233
246
  query.keys.select { |k| k.to_s =~ /^#{@range_key}\./ }.each do |key|
@@ -250,6 +263,8 @@ module Dynamoid #:nodoc:
250
263
  end
251
264
 
252
265
  def type_cast_condition_parameter(key, value)
266
+ return value if [:array, :set].include?(source.attributes[key.to_sym][:type])
267
+
253
268
  if !value.respond_to?(:to_ary)
254
269
  source.dump_field(value, source.attributes[key.to_sym])
255
270
  else
@@ -261,7 +276,7 @@ module Dynamoid #:nodoc:
261
276
  query_keys = query.keys.collect { |k| k.to_s.split('.').first }
262
277
 
263
278
  # See if querying based on table hash key
264
- if query_keys.include?(source.hash_key.to_s)
279
+ if query.keys.map(&:to_s).include?(source.hash_key.to_s)
265
280
  @hash_key = source.hash_key
266
281
 
267
282
  # Use table's default range key
@@ -286,7 +301,7 @@ module Dynamoid #:nodoc:
286
301
  # But only do so if projects ALL attributes otherwise we won't
287
302
  # get back full data
288
303
  source.global_secondary_indexes.each do |_, gsi|
289
- next unless query_keys.include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
304
+ next unless query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
290
305
  @hash_key = gsi.hash_key
291
306
  @range_key = gsi.range_key
292
307
  @index_name = gsi.name
@@ -301,21 +316,22 @@ module Dynamoid #:nodoc:
301
316
  # If using a secondary index then we must include the index's composite key
302
317
  # as well as the tables composite key.
303
318
  def start_key
319
+ return @start if @start.is_a?(Hash)
304
320
  hash_key = @hash_key || source.hash_key
305
321
  range_key = @range_key || source.range_key
306
322
 
307
323
  key = {}
308
- key[:hash_key_element] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
309
- key[:range_key_element] = type_cast_condition_parameter(range_key, @start.send(range_key)) if range_key
310
-
311
- # Add table composite keys if differ from secondary index used composite key
324
+ key[hash_key] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
325
+ if range_key
326
+ key[range_key] = type_cast_condition_parameter(range_key, @start.send(range_key))
327
+ end
328
+ # Add table composite keys if they differ from secondary index used composite key
312
329
  if hash_key != source.hash_key
313
- key[:table_hash_key_element] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
330
+ key[source.hash_key] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
314
331
  end
315
332
  if source.range_key && range_key != source.range_key
316
- key[:table_range_key_element] = type_cast_condition_parameter(source.range_key, @start.range_value)
333
+ key[source.range_key] = type_cast_condition_parameter(source.range_key, @start.range_value)
317
334
  end
318
-
319
335
  key
320
336
  end
321
337
 
@@ -326,7 +342,7 @@ module Dynamoid #:nodoc:
326
342
  opts[:record_limit] = @record_limit if @record_limit
327
343
  opts[:scan_limit] = @scan_limit if @scan_limit
328
344
  opts[:batch_size] = @batch_size if @batch_size
329
- opts[:next_token] = start_key if @start
345
+ opts[:exclusive_start_key] = start_key if @start
330
346
  opts[:scan_index_forward] = @scan_index_forward
331
347
  opts
332
348
  end
@@ -349,7 +365,7 @@ module Dynamoid #:nodoc:
349
365
  opts[:record_limit] = @record_limit if @record_limit
350
366
  opts[:scan_limit] = @scan_limit if @scan_limit
351
367
  opts[:batch_size] = @batch_size if @batch_size
352
- opts[:next_token] = start_key if @start
368
+ opts[:exclusive_start_key] = start_key if @start
353
369
  opts[:consistent_read] = true if @consistent_read
354
370
  opts
355
371
  end
@@ -5,7 +5,7 @@ module Dynamoid
5
5
 
6
6
  module ClassMethods
7
7
  def from_database(*)
8
- super.tap { |d| d.changed_attributes.clear }
8
+ super.tap { |d| d.send(:clear_changes_information) }
9
9
  end
10
10
  end
11
11
 
@@ -15,7 +15,7 @@ module Dynamoid
15
15
 
16
16
  def update!(*)
17
17
  ret = super
18
- clear_changes #update! completely reloads all fields on the class, so any extant changes are wiped out
18
+ clear_changes # update! completely reloads all fields on the class, so any extant changes are wiped out
19
19
  ret
20
20
  end
21
21
 
@@ -26,9 +26,9 @@ module Dynamoid
26
26
  def clear_changes
27
27
  previous = changes
28
28
  (block_given? ? yield : true).tap do |result|
29
- unless result == false #failed validation; nil is OK.
29
+ unless result == false # failed validation; nil is OK.
30
30
  @previously_changed = previous
31
- changed_attributes.clear
31
+ clear_changes_information
32
32
  end
33
33
  end
34
34
  end
@@ -43,5 +43,24 @@ module Dynamoid
43
43
  def attribute_method?(attr)
44
44
  super || self.class.attributes.has_key?(attr.to_sym)
45
45
  end
46
+
47
+ if ActiveModel::VERSION::STRING >= '5.2.0'
48
+ # The ActiveModel::Dirty API was changed
49
+ # https://github.com/rails/rails/commit/c3675f50d2e59b7fc173d7b332860c4b1a24a726#diff-aaddd42c7feb0834b1b5c66af69814d3
50
+ # So we just try to disable new functionality
51
+
52
+ def mutations_from_database
53
+ @mutations_from_database ||= ActiveModel::NullMutationTracker.instance
54
+ end
55
+
56
+ def forget_attribute_assignments
57
+ end
58
+ end
59
+
60
+ if ActiveModel::VERSION::STRING < '4.2.0'
61
+ def clear_changes_information
62
+ changed_attributes.clear
63
+ end
64
+ end
46
65
  end
47
66
  end
@@ -72,7 +72,11 @@ module Dynamoid #:nodoc:
72
72
  #
73
73
  # @since 0.2.0
74
74
  def create(attrs = {})
75
- build(attrs).tap(&:save)
75
+ if attrs.is_a?(Array)
76
+ attrs.map { |attr| create(attr) }
77
+ else
78
+ build(attrs).tap(&:save)
79
+ end
76
80
  end
77
81
 
78
82
  # Initialize a new object and immediately save it to the database. Raise an exception if persistence failed.
@@ -83,7 +87,11 @@ module Dynamoid #:nodoc:
83
87
  #
84
88
  # @since 0.2.0
85
89
  def create!(attrs = {})
86
- build(attrs).tap(&:save!)
90
+ if attrs.is_a?(Array)
91
+ attrs.map { |attr| create!(attr) }
92
+ else
93
+ build(attrs).tap(&:save!)
94
+ end
87
95
  end
88
96
 
89
97
  # Initialize a new object.
@@ -106,9 +114,84 @@ module Dynamoid #:nodoc:
106
114
  # @since 0.2.0
107
115
  def exists?(id_or_conditions = {})
108
116
  case id_or_conditions
109
- when Hash then ! where(id_or_conditions).all.empty?
110
- else !! find(id_or_conditions)
117
+ when Hash then where(id_or_conditions).first.present?
118
+ else !! find_by_id(id_or_conditions)
119
+ end
120
+ end
121
+
122
+ def update(hash_key, range_key_value=nil, attrs)
123
+ if range_key.present?
124
+ range_key_value = dump_field(range_key_value, attributes[self.range_key])
125
+ else
126
+ range_key_value = nil
127
+ end
128
+
129
+ model = find(hash_key, range_key: range_key_value, consistent_read: true)
130
+ model.update_attributes(attrs)
131
+ model
132
+ end
133
+
134
+ def update_fields(hash_key_value, range_key_value=nil, attrs={}, conditions={})
135
+ optional_params = [range_key_value, attrs, conditions].compact
136
+ if optional_params.first.is_a?(Hash)
137
+ range_key_value = nil
138
+ attrs, conditions = optional_params[0 .. 1]
139
+ else
140
+ range_key_value = optional_params.first
141
+ attrs, conditions = optional_params[1 .. 2]
142
+ end
143
+
144
+ options = if range_key
145
+ { range_key: dump_field(range_key_value, attributes[range_key]) }
146
+ else
147
+ {}
148
+ end
149
+
150
+ (conditions[:if_exists] ||= {})[hash_key] = hash_key_value
151
+ options[:conditions] = conditions
152
+
153
+ begin
154
+ new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
155
+ attrs.symbolize_keys.each do |k, v|
156
+ t.set k => dump_field(v, attributes[k])
157
+ end
158
+ end
159
+ new(new_attrs)
160
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
161
+ end
162
+ end
163
+
164
+ def upsert(hash_key_value, range_key_value=nil, attrs={}, conditions={})
165
+ optional_params = [range_key_value, attrs, conditions].compact
166
+ if optional_params.first.is_a?(Hash)
167
+ range_key_value = nil
168
+ attrs, conditions = optional_params[0 .. 1]
169
+ else
170
+ range_key_value = optional_params.first
171
+ attrs, conditions = optional_params[1 .. 2]
111
172
  end
173
+
174
+ options = if range_key
175
+ { range_key: dump_field(range_key_value, attributes[range_key]) }
176
+ else
177
+ {}
178
+ end
179
+
180
+ options[:conditions] = conditions
181
+
182
+ begin
183
+ new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
184
+ attrs.symbolize_keys.each do |k, v|
185
+ t.set k => dump_field(v, attributes[k])
186
+ end
187
+ end
188
+ new(new_attrs)
189
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
190
+ end
191
+ end
192
+
193
+ def deep_subclasses
194
+ subclasses + subclasses.map(&:deep_subclasses).flatten
112
195
  end
113
196
  end
114
197
 
@@ -167,7 +250,7 @@ module Dynamoid #:nodoc:
167
250
  # @since 0.2.0
168
251
  def reload
169
252
  range_key_value = range_value ? dumped_range_value : nil
170
- self.attributes = self.class.find(hash_key, :range_key => range_key_value, :consistent_read => true).attributes
253
+ self.attributes = self.class.find(hash_key, range_key: range_key_value, consistent_read: true).attributes
171
254
  @associations.values.each(&:reset)
172
255
  self
173
256
  end
@@ -27,7 +27,7 @@ module Dynamoid
27
27
  attr_reader :record
28
28
 
29
29
  def initialize(record)
30
- super("Failed to destroy item")
30
+ super('Failed to destroy item')
31
31
  @record = record
32
32
  end
33
33
  end
@@ -61,6 +61,9 @@ module Dynamoid
61
61
  end
62
62
  end
63
63
 
64
+ class RecordNotFound < Error
65
+ end
66
+
64
67
  class DocumentNotValid < Error
65
68
  attr_reader :document
66
69