dynamoid 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +9 -6
- data/Appraisals +8 -14
- data/CHANGELOG.md +24 -0
- data/README.md +493 -228
- data/gemfiles/rails_4_2.gemfile +5 -7
- data/gemfiles/rails_5_0.gemfile +4 -6
- data/gemfiles/rails_5_1.gemfile +4 -6
- data/gemfiles/rails_5_2.gemfile +4 -6
- data/gemfiles/rails_6_0.gemfile +8 -0
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +3 -10
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +25 -69
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +105 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +9 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +11 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +11 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +3 -2
- data/lib/dynamoid/components.rb +6 -3
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +33 -6
- data/lib/dynamoid/criteria/key_fields_detector.rb +101 -32
- data/lib/dynamoid/dirty.rb +186 -34
- data/lib/dynamoid/document.rb +8 -216
- data/lib/dynamoid/fields.rb +8 -0
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/persistence.rb +177 -85
- data/lib/dynamoid/persistence/import.rb +72 -0
- data/lib/dynamoid/persistence/save.rb +63 -0
- data/lib/dynamoid/persistence/update_fields.rb +62 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +9 -2
data/gemfiles/rails_4_2.gemfile
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file was generated by Appraisal
|
4
2
|
|
5
|
-
source
|
3
|
+
source "https://rubygems.org"
|
6
4
|
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
gem
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
6
|
+
gem "activemodel", "~> 4.2.0"
|
7
|
+
gem "nokogiri", "~> 1.6.8"
|
10
8
|
|
11
|
-
gemspec path:
|
9
|
+
gemspec path: "../"
|
data/gemfiles/rails_5_0.gemfile
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file was generated by Appraisal
|
4
2
|
|
5
|
-
source
|
3
|
+
source "https://rubygems.org"
|
6
4
|
|
7
|
-
gem
|
8
|
-
gem
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
6
|
+
gem "activemodel", "~> 5.0.0"
|
9
7
|
|
10
|
-
gemspec path:
|
8
|
+
gemspec path: "../"
|
data/gemfiles/rails_5_1.gemfile
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file was generated by Appraisal
|
4
2
|
|
5
|
-
source
|
3
|
+
source "https://rubygems.org"
|
6
4
|
|
7
|
-
gem
|
8
|
-
gem
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
6
|
+
gem "activemodel", "~> 5.1.0"
|
9
7
|
|
10
|
-
gemspec path:
|
8
|
+
gemspec path: "../"
|
data/gemfiles/rails_5_2.gemfile
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file was generated by Appraisal
|
4
2
|
|
5
|
-
source
|
3
|
+
source "https://rubygems.org"
|
6
4
|
|
7
|
-
gem
|
8
|
-
gem
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
6
|
+
gem "activemodel", "~> 5.2.0"
|
9
7
|
|
10
|
-
gemspec path:
|
8
|
+
gemspec path: "../"
|
data/lib/dynamoid.rb
CHANGED
data/lib/dynamoid/adapter.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
# require only 'concurrent/atom' once this issue is resolved:
|
4
4
|
# https://github.com/ruby-concurrency/concurrent-ruby/pull/377
|
5
5
|
require 'concurrent'
|
6
|
+
require "dynamoid/adapter_plugin/aws_sdk_v3"
|
6
7
|
|
7
8
|
# encoding: utf-8
|
8
9
|
module Dynamoid
|
@@ -29,7 +30,7 @@ module Dynamoid
|
|
29
30
|
def adapter
|
30
31
|
unless @adapter_.value
|
31
32
|
adapter = self.class.adapter_plugin_class.new
|
32
|
-
adapter.connect!
|
33
|
+
adapter.connect!
|
33
34
|
@adapter_.compare_and_set(nil, adapter)
|
34
35
|
clear_cache!
|
35
36
|
end
|
@@ -142,11 +143,7 @@ module Dynamoid
|
|
142
143
|
#
|
143
144
|
# @since 0.2.0
|
144
145
|
define_method(m) do |*args, &blk|
|
145
|
-
|
146
|
-
benchmark(m.to_s, *args) { adapter.send(m, *args, &blk) }
|
147
|
-
else
|
148
|
-
benchmark(m.to_s, *args) { adapter.send(m, *args) }
|
149
|
-
end
|
146
|
+
benchmark(m.to_s, *args) { adapter.send(m, *args, &blk) }
|
150
147
|
end
|
151
148
|
end
|
152
149
|
|
@@ -179,10 +176,6 @@ module Dynamoid
|
|
179
176
|
end
|
180
177
|
|
181
178
|
def self.adapter_plugin_class
|
182
|
-
unless Dynamoid.const_defined?(:AdapterPlugin) && Dynamoid::AdapterPlugin.const_defined?(Dynamoid::Config.adapter.camelcase)
|
183
|
-
require "dynamoid/adapter_plugin/#{Dynamoid::Config.adapter}"
|
184
|
-
end
|
185
|
-
|
186
179
|
Dynamoid::AdapterPlugin.const_get(Dynamoid::Config.adapter.camelcase)
|
187
180
|
end
|
188
181
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative 'aws_sdk_v3/query'
|
4
4
|
require_relative 'aws_sdk_v3/scan'
|
5
5
|
require_relative 'aws_sdk_v3/create_table'
|
6
|
+
require_relative 'aws_sdk_v3/batch_get_item'
|
6
7
|
require_relative 'aws_sdk_v3/item_updater'
|
7
8
|
require_relative 'aws_sdk_v3/table'
|
8
9
|
require_relative 'aws_sdk_v3/until_past_table_status'
|
@@ -22,8 +23,6 @@ module Dynamoid
|
|
22
23
|
range_eq: 'EQ'
|
23
24
|
}.freeze
|
24
25
|
|
25
|
-
# Don't implement NULL and NOT_NULL because it doesn't make seanse -
|
26
|
-
# we declare schema in models
|
27
26
|
FIELD_MAP = {
|
28
27
|
eq: 'EQ',
|
29
28
|
ne: 'NE',
|
@@ -35,7 +34,9 @@ module Dynamoid
|
|
35
34
|
between: 'BETWEEN',
|
36
35
|
in: 'IN',
|
37
36
|
contains: 'CONTAINS',
|
38
|
-
not_contains: 'NOT_CONTAINS'
|
37
|
+
not_contains: 'NOT_CONTAINS',
|
38
|
+
null: 'NULL',
|
39
|
+
not_null: 'NOT_NULL',
|
39
40
|
}.freeze
|
40
41
|
HASH_KEY = 'HASH'
|
41
42
|
RANGE_KEY = 'RANGE'
|
@@ -183,65 +184,11 @@ module Dynamoid
|
|
183
184
|
# @since 1.0.0
|
184
185
|
#
|
185
186
|
# @todo: Provide support for passing options to underlying batch_get_item
|
186
|
-
def batch_get_item(
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
ret = Hash.new([].freeze) # Default for tables where no rows are returned
|
191
|
-
|
192
|
-
table_ids.each do |t, ids|
|
193
|
-
next if ids.blank?
|
194
|
-
|
195
|
-
ids = Array(ids).dup
|
196
|
-
tbl = describe_table(t)
|
197
|
-
hk = tbl.hash_key.to_s
|
198
|
-
rng = tbl.range_key.to_s
|
199
|
-
|
200
|
-
while ids.present?
|
201
|
-
batch = ids.shift(Dynamoid::Config.batch_size)
|
202
|
-
|
203
|
-
request_items = Hash.new { |h, k| h[k] = [] }
|
204
|
-
|
205
|
-
keys = if rng.present?
|
206
|
-
Array(batch).map do |h, r|
|
207
|
-
{ hk => h, rng => r }
|
208
|
-
end
|
209
|
-
else
|
210
|
-
Array(batch).map do |id|
|
211
|
-
{ hk => id }
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
request_items[t] = {
|
216
|
-
keys: keys,
|
217
|
-
consistent_read: options[:consistent_read]
|
218
|
-
}
|
219
|
-
|
220
|
-
results = client.batch_get_item(
|
221
|
-
request_items: request_items
|
222
|
-
)
|
223
|
-
|
224
|
-
if block_given?
|
225
|
-
batch_results = Hash.new([].freeze)
|
226
|
-
|
227
|
-
results.data[:responses].each do |table, rows|
|
228
|
-
batch_results[table] += rows.collect { |r| result_item_to_hash(r) }
|
229
|
-
end
|
230
|
-
|
231
|
-
yield(batch_results, results.unprocessed_keys.present?)
|
232
|
-
else
|
233
|
-
results.data[:responses].each do |table, rows|
|
234
|
-
ret[table] += rows.collect { |r| result_item_to_hash(r) }
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
if results.unprocessed_keys.present?
|
239
|
-
ids += results.unprocessed_keys[t].keys.map { |h| h[hk] }
|
240
|
-
end
|
241
|
-
end
|
187
|
+
def batch_get_item(table_names_with_ids, options = {}, &block)
|
188
|
+
tables_with_ids = table_names_with_ids.transform_keys do |name|
|
189
|
+
describe_table(name)
|
242
190
|
end
|
243
|
-
|
244
|
-
ret unless block_given?
|
191
|
+
BatchGetItem.new(client, tables_with_ids, options).call(&block)
|
245
192
|
end
|
246
193
|
|
247
194
|
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
@@ -356,9 +303,14 @@ module Dynamoid
|
|
356
303
|
# @since 1.0.0
|
357
304
|
def delete_table(table_name, options = {})
|
358
305
|
resp = client.delete_table(table_name: table_name)
|
359
|
-
|
360
|
-
|
361
|
-
|
306
|
+
|
307
|
+
if options[:sync]
|
308
|
+
status = PARSE_TABLE_STATUS.call(resp, :table_description)
|
309
|
+
if status == TABLE_STATUSES[:deleting]
|
310
|
+
UntilPastTableStatus.new(client, table_name, :deleting).call
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
362
314
|
table_cache.delete(table_name)
|
363
315
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
364
316
|
Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
|
@@ -619,23 +571,27 @@ module Dynamoid
|
|
619
571
|
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
620
572
|
# @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
|
621
573
|
# @params [Object] value: scalar value or array/set
|
622
|
-
def attribute_value_list(operator, value)
|
623
|
-
self.class.attribute_value_list(operator, value)
|
624
|
-
end
|
625
|
-
|
626
574
|
def self.attribute_value_list(operator, value)
|
627
575
|
# For BETWEEN and IN operators we should keep value as is (it should be already an array)
|
576
|
+
# NULL and NOT_NULL require absence of attribute list
|
628
577
|
# For all the other operators we wrap the value with array
|
578
|
+
# https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/LegacyConditionalParameters.Conditions.html
|
629
579
|
if %w[BETWEEN IN].include?(operator)
|
630
580
|
[value].flatten
|
581
|
+
elsif %w[NULL NOT_NULL].include?(operator)
|
582
|
+
nil
|
631
583
|
else
|
632
584
|
[value]
|
633
585
|
end
|
634
586
|
end
|
635
587
|
|
636
588
|
def sanitize_item(attributes)
|
589
|
+
config_value = Dynamoid.config.store_attribute_with_nil_value
|
590
|
+
store_attribute_with_nil_value = config_value.nil? ? false : !!config_value
|
591
|
+
|
637
592
|
attributes.reject do |_, v|
|
638
|
-
|
593
|
+
((v.is_a?(Set) || v.is_a?(String)) && v.empty?) ||
|
594
|
+
(!store_attribute_with_nil_value && v.nil?)
|
639
595
|
end.transform_values do |v|
|
640
596
|
v.is_a?(Hash) ? v.stringify_keys : v
|
641
597
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module AdapterPlugin
|
3
|
+
class AwsSdkV3
|
4
|
+
# Documentation
|
5
|
+
# https://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
6
|
+
class BatchGetItem
|
7
|
+
attr_reader :client, :tables_with_ids, :options
|
8
|
+
|
9
|
+
def initialize(client, tables_with_ids, options = {})
|
10
|
+
@client = client
|
11
|
+
@tables_with_ids = tables_with_ids
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
results = {}
|
17
|
+
|
18
|
+
tables_with_ids.each do |table, ids|
|
19
|
+
if ids.blank?
|
20
|
+
results[table.name] = []
|
21
|
+
next
|
22
|
+
end
|
23
|
+
|
24
|
+
ids = Array(ids).dup
|
25
|
+
|
26
|
+
while ids.present?
|
27
|
+
batch = ids.shift(Dynamoid::Config.batch_size)
|
28
|
+
request = build_request(table, batch)
|
29
|
+
api_response = client.batch_get_item(request)
|
30
|
+
response = Response.new(api_response)
|
31
|
+
|
32
|
+
if block_given?
|
33
|
+
# return batch items as a result
|
34
|
+
batch_results = Hash.new([].freeze)
|
35
|
+
batch_results.update(response.items_grouped_by_table)
|
36
|
+
|
37
|
+
yield(batch_results, response.successful_partially?)
|
38
|
+
else
|
39
|
+
# collect all the batches to return at the end
|
40
|
+
results.update(response.items_grouped_by_table) { |_, its1, its2| its1 + its2 }
|
41
|
+
end
|
42
|
+
|
43
|
+
if response.successful_partially?
|
44
|
+
ids += response.unprocessed_ids(table)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
results unless block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def build_request(table, ids)
|
55
|
+
ids = Array(ids)
|
56
|
+
|
57
|
+
keys = if table.range_key.nil?
|
58
|
+
ids.map { |hk| { table.hash_key => hk } }
|
59
|
+
else
|
60
|
+
ids.map { |hk, rk| { table.hash_key => hk, table.range_key => rk } }
|
61
|
+
end
|
62
|
+
|
63
|
+
{
|
64
|
+
request_items: {
|
65
|
+
table.name => {
|
66
|
+
keys: keys,
|
67
|
+
consistent_read: options[:consistent_read]
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Helper class to work with response
|
74
|
+
class Response
|
75
|
+
def initialize(api_response)
|
76
|
+
@api_response = api_response
|
77
|
+
end
|
78
|
+
|
79
|
+
def successful_partially?
|
80
|
+
@api_response.unprocessed_keys.present?
|
81
|
+
end
|
82
|
+
|
83
|
+
def unprocessed_ids(table)
|
84
|
+
# unprocessed_keys Hash contains as values instances of
|
85
|
+
# Aws::DynamoDB::Types::KeysAndAttributes
|
86
|
+
@api_response.unprocessed_keys[table.name].keys.map { |h| h[table.hash_key.to_s] }
|
87
|
+
end
|
88
|
+
|
89
|
+
def items_grouped_by_table
|
90
|
+
# data[:responses] is a Hash[table_name -> items]
|
91
|
+
@api_response.data[:responses].transform_values do |items|
|
92
|
+
items.map(&method(:item_to_hash))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def item_to_hash(item)
|
99
|
+
item.symbolize_keys
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -62,11 +62,16 @@ module Dynamoid
|
|
62
62
|
end
|
63
63
|
resp = client.create_table(client_opts)
|
64
64
|
options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
|
66
|
+
if options[:sync]
|
67
|
+
status = PARSE_TABLE_STATUS.call(resp, :table_description)
|
68
|
+
if status == TABLE_STATUSES[:creating]
|
69
|
+
UntilPastTableStatus.new(client, table_name, :creating).call
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
68
73
|
# Response to original create_table, which, if options[:sync]
|
69
|
-
#
|
74
|
+
# may have an outdated table_description.table_status of "CREATING"
|
70
75
|
resp
|
71
76
|
end
|
72
77
|
|
@@ -11,6 +11,7 @@ module Dynamoid
|
|
11
11
|
OPTIONS_KEYS = %i[
|
12
12
|
limit hash_key hash_value range_key consistent_read scan_index_forward
|
13
13
|
select index_name batch_size exclusive_start_key record_limit scan_limit
|
14
|
+
project
|
14
15
|
].freeze
|
15
16
|
|
16
17
|
attr_reader :client, :table, :options, :conditions
|
@@ -63,10 +64,11 @@ module Dynamoid
|
|
63
64
|
batch_size = options[:batch_size]
|
64
65
|
limit = [record_limit, scan_limit, batch_size].compact.min
|
65
66
|
|
66
|
-
request[:limit]
|
67
|
-
request[:table_name]
|
68
|
-
request[:key_conditions]
|
69
|
-
request[:query_filter]
|
67
|
+
request[:limit] = limit if limit
|
68
|
+
request[:table_name] = table.name
|
69
|
+
request[:key_conditions] = key_conditions
|
70
|
+
request[:query_filter] = query_filter
|
71
|
+
request[:attributes_to_get] = attributes_to_get
|
70
72
|
|
71
73
|
request
|
72
74
|
end
|
@@ -117,6 +119,11 @@ module Dynamoid
|
|
117
119
|
result
|
118
120
|
end
|
119
121
|
end
|
122
|
+
|
123
|
+
def attributes_to_get
|
124
|
+
return if options[:project].nil?
|
125
|
+
options[:project].map(&:to_s)
|
126
|
+
end
|
120
127
|
end
|
121
128
|
end
|
122
129
|
end
|
@@ -54,9 +54,10 @@ module Dynamoid
|
|
54
54
|
batch_size = options[:batch_size]
|
55
55
|
limit = [record_limit, scan_limit, batch_size].compact.min
|
56
56
|
|
57
|
-
request[:limit]
|
58
|
-
request[:table_name]
|
59
|
-
request[:scan_filter]
|
57
|
+
request[:limit] = limit if limit
|
58
|
+
request[:table_name] = table.name
|
59
|
+
request[:scan_filter] = scan_filter
|
60
|
+
request[:attributes_to_get] = attributes_to_get
|
60
61
|
|
61
62
|
request
|
62
63
|
end
|
@@ -75,10 +76,17 @@ module Dynamoid
|
|
75
76
|
comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
|
76
77
|
attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
|
77
78
|
}
|
79
|
+
# nil means operator doesn't require attribute value list
|
80
|
+
conditions.delete(:attribute_value_list) if conditions[:attribute_value_list].nil?
|
78
81
|
result[attr] = condition
|
79
82
|
result
|
80
83
|
end
|
81
84
|
end
|
85
|
+
|
86
|
+
def attributes_to_get
|
87
|
+
return if options[:project].nil?
|
88
|
+
options[:project].map(&:to_s)
|
89
|
+
end
|
82
90
|
end
|
83
91
|
end
|
84
92
|
end
|