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