dynamoid 1.3.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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