dynamoid 1.3.4 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +3 -0
- data/.travis.yml +37 -7
- data/Appraisals +11 -0
- data/CHANGELOG.md +115 -2
- data/Gemfile +2 -0
- data/LICENSE.txt +18 -16
- data/README.md +253 -34
- data/Rakefile +0 -24
- data/Vagrantfile +1 -1
- data/docker-compose.yml +7 -0
- data/dynamoid.gemspec +4 -4
- data/gemfiles/rails_4_0.gemfile +3 -3
- data/gemfiles/rails_4_1.gemfile +3 -3
- data/gemfiles/rails_4_2.gemfile +3 -3
- data/gemfiles/rails_5_0.gemfile +2 -1
- data/gemfiles/rails_5_1.gemfile +8 -0
- data/gemfiles/rails_5_2.gemfile +8 -0
- data/lib/dynamoid.rb +31 -31
- data/lib/dynamoid/adapter.rb +14 -10
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +188 -100
- data/lib/dynamoid/associations.rb +21 -12
- data/lib/dynamoid/associations/association.rb +19 -3
- data/lib/dynamoid/associations/belongs_to.rb +26 -16
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -16
- data/lib/dynamoid/associations/has_many.rb +2 -17
- data/lib/dynamoid/associations/has_one.rb +0 -14
- data/lib/dynamoid/associations/many_association.rb +19 -6
- data/lib/dynamoid/associations/single_association.rb +25 -7
- data/lib/dynamoid/config.rb +37 -18
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +11 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +25 -0
- data/lib/dynamoid/config/options.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +48 -32
- data/lib/dynamoid/dirty.rb +23 -4
- data/lib/dynamoid/document.rb +88 -5
- data/lib/dynamoid/errors.rb +4 -1
- data/lib/dynamoid/fields.rb +6 -6
- data/lib/dynamoid/finders.rb +42 -12
- data/lib/dynamoid/identity_map.rb +0 -1
- data/lib/dynamoid/indexes.rb +41 -54
- data/lib/dynamoid/persistence.rb +151 -40
- data/lib/dynamoid/railtie.rb +1 -1
- data/lib/dynamoid/validations.rb +4 -3
- data/lib/dynamoid/version.rb +1 -1
- metadata +18 -29
- data/gemfiles/rails_4_0.gemfile.lock +0 -150
- data/gemfiles/rails_4_1.gemfile.lock +0 -154
- data/gemfiles/rails_4_2.gemfile.lock +0 -175
- data/gemfiles/rails_5_0.gemfile.lock +0 -180
@@ -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
|
@@ -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
|
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,
|
83
|
+
Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence)
|
77
84
|
else
|
78
|
-
Dynamoid.adapter.scan(source.table_name,
|
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
|
-
|
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
|
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
|
-
{ :
|
183
|
+
{ range_greater_than: val }
|
177
184
|
when 'lt'
|
178
|
-
{ :
|
185
|
+
{ range_less_than: val }
|
179
186
|
when 'gte'
|
180
|
-
{ :
|
187
|
+
{ range_gte: val }
|
181
188
|
when 'lte'
|
182
|
-
{ :
|
189
|
+
{ range_lte: val }
|
183
190
|
when 'between'
|
184
|
-
{ :
|
191
|
+
{ range_between: val }
|
185
192
|
when 'begins_with'
|
186
|
-
{ :
|
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(:
|
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
|
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
|
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[
|
309
|
-
|
310
|
-
|
311
|
-
|
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[
|
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[
|
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[:
|
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[:
|
368
|
+
opts[:exclusive_start_key] = start_key if @start
|
353
369
|
opts[:consistent_read] = true if @consistent_read
|
354
370
|
opts
|
355
371
|
end
|
data/lib/dynamoid/dirty.rb
CHANGED
@@ -5,7 +5,7 @@ module Dynamoid
|
|
5
5
|
|
6
6
|
module ClassMethods
|
7
7
|
def from_database(*)
|
8
|
-
super.tap { |d| d.
|
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
|
-
|
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
|
data/lib/dynamoid/document.rb
CHANGED
@@ -72,7 +72,11 @@ module Dynamoid #:nodoc:
|
|
72
72
|
#
|
73
73
|
# @since 0.2.0
|
74
74
|
def create(attrs = {})
|
75
|
-
|
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
|
-
|
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
|
110
|
-
else !!
|
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, :
|
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
|
data/lib/dynamoid/errors.rb
CHANGED
@@ -27,7 +27,7 @@ module Dynamoid
|
|
27
27
|
attr_reader :record
|
28
28
|
|
29
29
|
def initialize(record)
|
30
|
-
super(
|
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
|
|