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
data/Rakefile
CHANGED
@@ -18,30 +18,6 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
18
18
|
spec.pattern = FileList["spec/**/*_spec.rb"]
|
19
19
|
end
|
20
20
|
|
21
|
-
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
22
|
-
spec.pattern = "spec/**/*_spec.rb"
|
23
|
-
spec.rcov = true
|
24
|
-
end
|
25
|
-
|
26
|
-
desc "Start DynamoDBLocal, run tests, clean up"
|
27
|
-
task :unattended_spec do |t|
|
28
|
-
|
29
|
-
if system("bin/start_dynamodblocal")
|
30
|
-
puts "DynamoDBLocal started; proceeding with specs."
|
31
|
-
else
|
32
|
-
raise "Unable to start DynamoDBLocal. Cannot run unattended specs."
|
33
|
-
end
|
34
|
-
|
35
|
-
#Cleanup
|
36
|
-
at_exit do
|
37
|
-
unless system("bin/stop_dynamodblocal")
|
38
|
-
$stderr.puts "Unable to cleanly stop DynamoDBLocal."
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
Rake::Task["spec"].invoke
|
43
|
-
end
|
44
|
-
|
45
21
|
require "yard"
|
46
22
|
YARD::Rake::YardocTask.new do |t|
|
47
23
|
t.files = ["lib/**/*.rb", "README", "LICENSE"] # optional
|
data/Vagrantfile
CHANGED
data/docker-compose.yml
ADDED
data/dynamoid.gemspec
CHANGED
@@ -21,7 +21,8 @@ Gem::Specification.new do |spec|
|
|
21
21
|
"Sumanth Ravipati",
|
22
22
|
"Pascal Corpet",
|
23
23
|
"Brian Glusman",
|
24
|
-
"Peter Boling"
|
24
|
+
"Peter Boling",
|
25
|
+
"Andrew Konchin"
|
25
26
|
]
|
26
27
|
spec.email = ["peter.boling@gmail.com", "brian@stellaservice.com"]
|
27
28
|
|
@@ -50,13 +51,12 @@ Gem::Specification.new do |spec|
|
|
50
51
|
spec.add_development_dependency(%q<activesupport>, [">= 4"])
|
51
52
|
spec.add_runtime_dependency(%q<aws-sdk-resources>, ["~> 2"])
|
52
53
|
spec.add_runtime_dependency(%q<concurrent-ruby>, [">= 1.0"])
|
53
|
-
spec.add_development_dependency "pry"
|
54
|
+
spec.add_development_dependency "pry"
|
54
55
|
spec.add_development_dependency "bundler", "~> 1.14"
|
55
56
|
spec.add_development_dependency "rake", "~> 12.0"
|
56
57
|
spec.add_development_dependency "rspec", "~> 3.0"
|
57
58
|
spec.add_development_dependency "appraisal", "~> 2.1"
|
58
59
|
spec.add_development_dependency "wwtd", "~> 1.3"
|
59
60
|
spec.add_development_dependency(%q<yard>, [">= 0"])
|
60
|
-
spec.add_development_dependency
|
61
|
-
spec.add_development_dependency(%q<rspec-retry>, [">= 0"])
|
61
|
+
spec.add_development_dependency "coveralls", "~> 0.8"
|
62
62
|
end
|
data/gemfiles/rails_4_0.gemfile
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
5
6
|
gem "rails", "~> 4.0.0"
|
6
|
-
|
7
|
-
gem "nokogiri", "~> 1.6.8.1"
|
7
|
+
gem "nokogiri", "~> 1.6.8"
|
8
8
|
|
9
|
-
gemspec :
|
9
|
+
gemspec path: "../"
|
data/gemfiles/rails_4_1.gemfile
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
5
6
|
gem "rails", "~> 4.1.0"
|
6
|
-
|
7
|
-
gem "nokogiri", "~> 1.6.8.1"
|
7
|
+
gem "nokogiri", "~> 1.6.8"
|
8
8
|
|
9
|
-
gemspec :
|
9
|
+
gemspec path: "../"
|
data/gemfiles/rails_4_2.gemfile
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
gem "pry-byebug", platforms: :ruby
|
5
6
|
gem "rails", "~> 4.2.0"
|
6
|
-
|
7
|
-
gem "nokogiri", "~> 1.6.8.1"
|
7
|
+
gem "nokogiri", "~> 1.6.8"
|
8
8
|
|
9
|
-
gemspec :
|
9
|
+
gemspec path: "../"
|
data/gemfiles/rails_5_0.gemfile
CHANGED
data/lib/dynamoid.rb
CHANGED
@@ -1,36 +1,36 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
24
|
-
require
|
25
|
-
require
|
26
|
-
require
|
27
|
-
|
28
|
-
require
|
29
|
-
|
30
|
-
require
|
1
|
+
require 'delegate'
|
2
|
+
require 'time'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
require 'active_support/json'
|
7
|
+
require 'active_support/inflector'
|
8
|
+
require 'active_support/lazy_load_hooks'
|
9
|
+
require 'active_support/time_with_zone'
|
10
|
+
require 'active_model'
|
11
|
+
|
12
|
+
require 'dynamoid/version'
|
13
|
+
require 'dynamoid/errors'
|
14
|
+
require 'dynamoid/fields'
|
15
|
+
require 'dynamoid/indexes'
|
16
|
+
require 'dynamoid/associations'
|
17
|
+
require 'dynamoid/persistence'
|
18
|
+
require 'dynamoid/dirty'
|
19
|
+
require 'dynamoid/validations'
|
20
|
+
require 'dynamoid/criteria'
|
21
|
+
require 'dynamoid/finders'
|
22
|
+
require 'dynamoid/identity_map'
|
23
|
+
require 'dynamoid/config'
|
24
|
+
require 'dynamoid/components'
|
25
|
+
require 'dynamoid/document'
|
26
|
+
require 'dynamoid/adapter'
|
27
|
+
|
28
|
+
require 'dynamoid/tasks/database'
|
29
|
+
|
30
|
+
require 'dynamoid/middleware/identity_map'
|
31
31
|
|
32
32
|
if defined?(Rails)
|
33
|
-
require
|
33
|
+
require 'dynamoid/railtie'
|
34
34
|
end
|
35
35
|
|
36
36
|
module Dynamoid
|
data/lib/dynamoid/adapter.rb
CHANGED
@@ -51,7 +51,7 @@ module Dynamoid
|
|
51
51
|
def benchmark(method, *args)
|
52
52
|
start = Time.now
|
53
53
|
result = yield
|
54
|
-
Dynamoid.logger.
|
54
|
+
Dynamoid.logger.debug "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
|
55
55
|
return result
|
56
56
|
end
|
57
57
|
|
@@ -80,12 +80,12 @@ module Dynamoid
|
|
80
80
|
# unless multiple ids are passed in.
|
81
81
|
#
|
82
82
|
# @since 0.2.0
|
83
|
-
def read(table, ids, options = {})
|
83
|
+
def read(table, ids, options = {}, &blk)
|
84
84
|
range_key = options.delete(:range_key)
|
85
85
|
|
86
86
|
if ids.respond_to?(:each)
|
87
87
|
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
88
|
-
batch_get_item({table => ids}, options)
|
88
|
+
batch_get_item({table => ids}, options, &blk)
|
89
89
|
else
|
90
90
|
options[:range_key] = range_key if range_key
|
91
91
|
get_item(table, ids, options)
|
@@ -99,13 +99,13 @@ module Dynamoid
|
|
99
99
|
# @param [Array] range_key of the record to delete, can also be a string of just one range_key
|
100
100
|
#
|
101
101
|
def delete(table, ids, options = {})
|
102
|
-
range_key = options[:range_key] #array of range keys that matches the ids passed in
|
102
|
+
range_key = options[:range_key] # array of range keys that matches the ids passed in
|
103
103
|
if ids.respond_to?(:each)
|
104
104
|
if range_key.respond_to?(:each)
|
105
|
-
#turn ids into array of arrays each element being hash_key, range_key
|
106
|
-
ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
|
105
|
+
# turn ids into array of arrays each element being hash_key, range_key
|
106
|
+
ids = ids.each_with_index.map{|id, i| [id, range_key[i]]}
|
107
107
|
else
|
108
|
-
ids = range_key ? [
|
108
|
+
ids = range_key ? ids.map { |id| [id, range_key] } : ids
|
109
109
|
end
|
110
110
|
|
111
111
|
batch_delete_item(table => ids)
|
@@ -120,7 +120,7 @@ module Dynamoid
|
|
120
120
|
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
121
121
|
#
|
122
122
|
# @since 0.2.0
|
123
|
-
def scan(table, query, opts = {})
|
123
|
+
def scan(table, query = {}, opts = {})
|
124
124
|
benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
125
125
|
end
|
126
126
|
|
@@ -144,8 +144,12 @@ module Dynamoid
|
|
144
144
|
# Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
|
145
145
|
#
|
146
146
|
# @since 0.2.0
|
147
|
-
define_method(m) do |*args|
|
148
|
-
|
147
|
+
define_method(m) do |*args, &blk|
|
148
|
+
if blk.present?
|
149
|
+
benchmark("#{m.to_s}", *args) { adapter.send(m, *args, &blk) }
|
150
|
+
else
|
151
|
+
benchmark("#{m.to_s}", *args) { adapter.send(m, *args) }
|
152
|
+
end
|
149
153
|
end
|
150
154
|
end
|
151
155
|
|
@@ -3,7 +3,7 @@ module Dynamoid
|
|
3
3
|
|
4
4
|
# The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby.
|
5
5
|
class AwsSdkV2
|
6
|
-
EQ =
|
6
|
+
EQ = 'EQ'.freeze
|
7
7
|
RANGE_MAP = {
|
8
8
|
range_greater_than: 'GT',
|
9
9
|
range_less_than: 'LT',
|
@@ -18,6 +18,7 @@ module Dynamoid
|
|
18
18
|
# we declare schema in models
|
19
19
|
FIELD_MAP = {
|
20
20
|
eq: 'EQ',
|
21
|
+
ne: 'NE',
|
21
22
|
gt: 'GT',
|
22
23
|
lt: 'LT',
|
23
24
|
gte: 'GE',
|
@@ -28,16 +29,16 @@ module Dynamoid
|
|
28
29
|
contains: 'CONTAINS',
|
29
30
|
not_contains: 'NOT_CONTAINS'
|
30
31
|
}
|
31
|
-
HASH_KEY =
|
32
|
-
RANGE_KEY =
|
33
|
-
STRING_TYPE =
|
34
|
-
NUM_TYPE =
|
35
|
-
BINARY_TYPE =
|
32
|
+
HASH_KEY = 'HASH'.freeze
|
33
|
+
RANGE_KEY = 'RANGE'.freeze
|
34
|
+
STRING_TYPE = 'S'.freeze
|
35
|
+
NUM_TYPE = 'N'.freeze
|
36
|
+
BINARY_TYPE = 'B'.freeze
|
36
37
|
TABLE_STATUSES = {
|
37
|
-
creating:
|
38
|
-
updating:
|
39
|
-
deleting:
|
40
|
-
active:
|
38
|
+
creating: 'CREATING',
|
39
|
+
updating: 'UPDATING',
|
40
|
+
deleting: 'DELETING',
|
41
|
+
active: 'ACTIVE'
|
41
42
|
}.freeze
|
42
43
|
PARSE_TABLE_STATUS = ->(resp, lookup = :table) {
|
43
44
|
# lookup is table for describe_table API
|
@@ -45,6 +46,8 @@ module Dynamoid
|
|
45
46
|
# because Amazon, damnit.
|
46
47
|
resp.send(lookup).table_status
|
47
48
|
}
|
49
|
+
BATCH_WRITE_ITEM_REQUESTS_LIMIT = 25
|
50
|
+
|
48
51
|
attr_reader :table_cache
|
49
52
|
|
50
53
|
# Establish the connection to DynamoDB.
|
@@ -81,30 +84,55 @@ module Dynamoid
|
|
81
84
|
@client
|
82
85
|
end
|
83
86
|
|
84
|
-
# Puts
|
87
|
+
# Puts multiple items in one table
|
88
|
+
#
|
89
|
+
# If optional block is passed it will be called for each written batch of items, meaning once per batch.
|
90
|
+
# Block receives boolean flag which is true if there are some unprocessed items, otherwise false.
|
91
|
+
#
|
92
|
+
# @example Saves several items to the table testtable
|
93
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', [{ id: '1', name: 'a' }, { id: '2', name: 'b'}])
|
94
|
+
#
|
95
|
+
# @example Pass block
|
96
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', items) do |bool|
|
97
|
+
# if bool
|
98
|
+
# puts 'there are unprocessed items'
|
99
|
+
# end
|
100
|
+
# end
|
85
101
|
#
|
86
102
|
# @param [String] table_name the name of the table
|
87
103
|
# @param [Array] items to be processed
|
88
104
|
# @param [Hash] additional options
|
105
|
+
# @param [Proc] optional block
|
89
106
|
#
|
90
|
-
#See:
|
107
|
+
# See:
|
108
|
+
# * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
|
109
|
+
# * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
|
91
110
|
def batch_write_item table_name, objects, options = {}
|
92
|
-
|
93
|
-
options ||= {}
|
94
|
-
objects.each do |o|
|
95
|
-
request_items << { "put_request" => { item: o } }
|
96
|
-
end
|
111
|
+
items = objects.map { |o| sanitize_item(o) }
|
97
112
|
|
98
113
|
begin
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
114
|
+
while items.present? do
|
115
|
+
batch = items.shift(BATCH_WRITE_ITEM_REQUESTS_LIMIT)
|
116
|
+
requests = batch.map { |item| { put_request: { item: item } } }
|
117
|
+
|
118
|
+
response = client.batch_write_item(
|
119
|
+
{
|
120
|
+
request_items: {
|
121
|
+
table_name => requests,
|
122
|
+
},
|
123
|
+
return_consumed_capacity: 'TOTAL',
|
124
|
+
return_item_collection_metrics: 'SIZE'
|
125
|
+
}.merge!(options)
|
126
|
+
)
|
127
|
+
|
128
|
+
if block_given?
|
129
|
+
yield(response.unprocessed_items.present?)
|
130
|
+
end
|
131
|
+
|
132
|
+
if response.unprocessed_items.present?
|
133
|
+
items += response.unprocessed_items[table_name].map { |r| r.put_request.item }
|
134
|
+
end
|
135
|
+
end
|
108
136
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
109
137
|
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
110
138
|
end
|
@@ -112,17 +140,37 @@ module Dynamoid
|
|
112
140
|
|
113
141
|
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
114
142
|
#
|
143
|
+
# If optional block is passed `nil` will be returned and the block will be called for each read batch of items,
|
144
|
+
# meaning once per batch.
|
145
|
+
#
|
146
|
+
# Block receives parameters:
|
147
|
+
# * hash with items like `{ table_name: [items]}`
|
148
|
+
# * and boolean flag is true if there are some unprocessed keys, otherwise false.
|
149
|
+
#
|
115
150
|
# @example Retrieve IDs 1 and 2 from the table testtable
|
116
|
-
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item(
|
151
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ['1', '2'])
|
152
|
+
#
|
153
|
+
# @example Pass block to receive each batch
|
154
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ids) do |hash, bool|
|
155
|
+
# puts hash['table1']
|
156
|
+
#
|
157
|
+
# if bool
|
158
|
+
# puts 'there are unprocessed keys'
|
159
|
+
# end
|
160
|
+
# end
|
117
161
|
#
|
118
162
|
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
119
163
|
# @param [Hash] options to be passed to underlying BatchGet call
|
164
|
+
# @param [Proc] optional block can be passed to handle each batch of items
|
120
165
|
#
|
121
166
|
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
122
167
|
#
|
168
|
+
# See:
|
169
|
+
# * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
170
|
+
#
|
123
171
|
# @since 1.0.0
|
124
172
|
#
|
125
|
-
# @todo: Provide support for passing options to underlying batch_get_item
|
173
|
+
# @todo: Provide support for passing options to underlying batch_get_item
|
126
174
|
def batch_get_item(table_ids, options = {})
|
127
175
|
request_items = Hash.new{|h, k| h[k] = []}
|
128
176
|
return request_items if table_ids.all?{|k, v| v.blank?}
|
@@ -131,19 +179,22 @@ module Dynamoid
|
|
131
179
|
|
132
180
|
table_ids.each do |t, ids|
|
133
181
|
next if ids.blank?
|
182
|
+
ids = Array(ids).dup
|
134
183
|
tbl = describe_table(t)
|
135
184
|
hk = tbl.hash_key.to_s
|
136
185
|
rng = tbl.range_key.to_s
|
137
186
|
|
138
|
-
|
187
|
+
while ids.present? do
|
188
|
+
batch = ids.shift(Dynamoid::Config.batch_size)
|
189
|
+
|
139
190
|
request_items = Hash.new{|h, k| h[k] = []}
|
140
191
|
|
141
192
|
keys = if rng.present?
|
142
|
-
Array(
|
193
|
+
Array(batch).map do |h, r|
|
143
194
|
{ hk => h, rng => r }
|
144
195
|
end
|
145
196
|
else
|
146
|
-
Array(
|
197
|
+
Array(batch).map do |id|
|
147
198
|
{ hk => id }
|
148
199
|
end
|
149
200
|
end
|
@@ -156,35 +207,70 @@ module Dynamoid
|
|
156
207
|
request_items: request_items
|
157
208
|
)
|
158
209
|
|
159
|
-
|
160
|
-
|
210
|
+
unless block_given?
|
211
|
+
results.data[:responses].each do |table, rows|
|
212
|
+
ret[table] += rows.collect { |r| result_item_to_hash(r) }
|
213
|
+
end
|
214
|
+
else
|
215
|
+
batch_results = Hash.new([].freeze)
|
216
|
+
|
217
|
+
results.data[:responses].each do |table, rows|
|
218
|
+
batch_results[table] += rows.collect { |r| result_item_to_hash(r) }
|
219
|
+
end
|
220
|
+
|
221
|
+
yield(batch_results, results.unprocessed_keys.present?)
|
222
|
+
end
|
223
|
+
|
224
|
+
if results.unprocessed_keys.present?
|
225
|
+
ids += results.unprocessed_keys[t].keys.map { |h| h[hk] }
|
161
226
|
end
|
162
227
|
end
|
163
228
|
end
|
164
229
|
|
165
|
-
|
230
|
+
unless block_given?
|
231
|
+
ret
|
232
|
+
end
|
166
233
|
end
|
167
234
|
|
168
235
|
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
169
236
|
#
|
170
237
|
# @example Delete IDs 1 and 2 from the table testtable
|
171
238
|
# Dynamoid::AdapterPlugin::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
172
|
-
#or
|
239
|
+
# or
|
173
240
|
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
174
241
|
#
|
175
242
|
# @param [Hash] options the hash of tables and IDs to delete
|
176
243
|
#
|
177
|
-
#
|
244
|
+
# See:
|
245
|
+
# * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
|
246
|
+
# * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
|
178
247
|
#
|
179
|
-
#
|
248
|
+
# TODO handle rejections because of internal processing failures
|
180
249
|
def batch_delete_item(options)
|
250
|
+
requests = []
|
251
|
+
|
181
252
|
options.each_pair do |table_name, ids|
|
182
253
|
table = describe_table(table_name)
|
183
|
-
|
184
|
-
|
254
|
+
|
255
|
+
ids.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |sliced_ids|
|
256
|
+
delete_requests = sliced_ids.map { |id|
|
257
|
+
{delete_request: {key: key_stanza(table, *id)}}
|
258
|
+
}
|
259
|
+
|
260
|
+
requests << {table_name => delete_requests}
|
185
261
|
end
|
186
262
|
end
|
187
|
-
|
263
|
+
|
264
|
+
begin
|
265
|
+
requests.map do |request_items|
|
266
|
+
client.batch_write_item(
|
267
|
+
request_items: request_items,
|
268
|
+
return_consumed_capacity: 'TOTAL',
|
269
|
+
return_item_collection_metrics: 'SIZE')
|
270
|
+
end
|
271
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
272
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
273
|
+
end
|
188
274
|
end
|
189
275
|
|
190
276
|
# Create a table on DynamoDB. This usually takes a long time to complete.
|
@@ -210,8 +296,8 @@ module Dynamoid
|
|
210
296
|
gs_indexes = options[:global_secondary_indexes]
|
211
297
|
|
212
298
|
key_schema = {
|
213
|
-
:
|
214
|
-
:
|
299
|
+
hash_key_schema: { key => (options[:hash_key_type] || :string) },
|
300
|
+
range_key_schema: options[:range_key]
|
215
301
|
}
|
216
302
|
attribute_definitions = build_all_attribute_definitions(
|
217
303
|
key_schema,
|
@@ -367,7 +453,7 @@ module Dynamoid
|
|
367
453
|
key: key_stanza(table, key, range_key),
|
368
454
|
attribute_updates: iu.to_h,
|
369
455
|
expected: expected_stanza(conditions),
|
370
|
-
return_values:
|
456
|
+
return_values: 'ALL_NEW'
|
371
457
|
)
|
372
458
|
result_item_to_hash(result[:attributes])
|
373
459
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
@@ -393,13 +479,8 @@ module Dynamoid
|
|
393
479
|
#
|
394
480
|
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
395
481
|
def put_item(table_name, object, options = {})
|
396
|
-
item = {}
|
397
482
|
options ||= {}
|
398
|
-
|
399
|
-
object.each do |k, v|
|
400
|
-
next if v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
401
|
-
item[k.to_s] = v
|
402
|
-
end
|
483
|
+
item = sanitize_item(object)
|
403
484
|
|
404
485
|
begin
|
405
486
|
client.put_item(
|
@@ -452,52 +533,34 @@ module Dynamoid
|
|
452
533
|
record_limit = opts.delete(:record_limit)
|
453
534
|
scan_limit = opts.delete(:scan_limit)
|
454
535
|
batch_size = opts.delete(:batch_size)
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
opts.delete(:next_token).tap do |token|
|
459
|
-
break unless token
|
460
|
-
q[:exclusive_start_key] = {
|
461
|
-
hk => token[:hash_key_element],
|
462
|
-
rng => token[:range_key_element]
|
463
|
-
}
|
464
|
-
# For secondary indices the start key must contain the indices composite key
|
465
|
-
# but also the table's composite keys
|
466
|
-
q[:exclusive_start_key][table.hash_key] = token[:table_hash_key_element] if token[:table_hash_key_element]
|
467
|
-
q[:exclusive_start_key][table.range_key] = token[:table_range_key_element] if token[:table_range_key_element]
|
468
|
-
end
|
536
|
+
exclusive_start_key = opts.delete(:exclusive_start_key)
|
537
|
+
limit = [record_limit, scan_limit, batch_size].compact.min
|
469
538
|
|
470
539
|
key_conditions = {
|
471
540
|
hk => {
|
472
|
-
# TODO: Provide option for other operators like NE, IN, LE, etc
|
473
541
|
comparison_operator: EQ,
|
474
|
-
attribute_value_list:
|
475
|
-
opts.delete(:hash_value).freeze
|
476
|
-
]
|
542
|
+
attribute_value_list: attribute_value_list(EQ, opts.delete(:hash_value).freeze)
|
477
543
|
}
|
478
544
|
}
|
479
545
|
|
480
546
|
opts.each_pair do |k, v|
|
481
|
-
# TODO: ATM, only few comparison operators are supported, provide support for all operators
|
482
547
|
next unless(op = RANGE_MAP[k])
|
483
548
|
key_conditions[rng] = {
|
484
549
|
comparison_operator: op,
|
485
|
-
attribute_value_list:
|
486
|
-
opts.delete(k).freeze
|
487
|
-
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
550
|
+
attribute_value_list: attribute_value_list(op, opts.delete(k).freeze)
|
488
551
|
}
|
489
552
|
end
|
490
553
|
|
491
554
|
query_filter = {}
|
492
|
-
opts.reject {|k,_| k.in? RANGE_MAP.keys}.each do |attr, hash|
|
555
|
+
opts.reject {|k, _| k.in? RANGE_MAP.keys}.each do |attr, hash|
|
493
556
|
query_filter[attr] = {
|
494
557
|
comparison_operator: FIELD_MAP[hash.keys[0]],
|
495
|
-
attribute_value_list: [
|
496
|
-
hash.values[0].freeze
|
497
|
-
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
558
|
+
attribute_value_list: attribute_value_list(FIELD_MAP[hash.keys[0]], hash.values[0].freeze)
|
498
559
|
}
|
499
560
|
end
|
500
561
|
|
562
|
+
q[:limit] = limit if limit
|
563
|
+
q[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
501
564
|
q[:table_name] = table_name
|
502
565
|
q[:key_conditions] = key_conditions
|
503
566
|
q[:query_filter] = query_filter
|
@@ -557,7 +620,7 @@ module Dynamoid
|
|
557
620
|
# @since 1.0.0
|
558
621
|
#
|
559
622
|
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
560
|
-
def scan(table_name, scan_hash, select_opts = {})
|
623
|
+
def scan(table_name, scan_hash = {}, select_opts = {})
|
561
624
|
request = { table_name: table_name }
|
562
625
|
request[:consistent_read] = true if select_opts.delete(:consistent_read)
|
563
626
|
|
@@ -565,15 +628,16 @@ module Dynamoid
|
|
565
628
|
record_limit = select_opts.delete(:record_limit)
|
566
629
|
scan_limit = select_opts.delete(:scan_limit)
|
567
630
|
batch_size = select_opts.delete(:batch_size)
|
631
|
+
exclusive_start_key = select_opts.delete(:exclusive_start_key)
|
568
632
|
request_limit = [record_limit, scan_limit, batch_size].compact.min
|
569
633
|
request[:limit] = request_limit if request_limit
|
570
|
-
|
634
|
+
request[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
635
|
+
|
571
636
|
if scan_hash.present?
|
572
637
|
request[:scan_filter] = scan_hash.reduce({}) do |memo, (attr, cond)|
|
573
|
-
# Flatten as BETWEEN operator specifies array of two elements
|
574
638
|
memo.merge(attr.to_s => {
|
575
639
|
comparison_operator: FIELD_MAP[cond.keys[0]],
|
576
|
-
attribute_value_list: [cond.values[0].freeze
|
640
|
+
attribute_value_list: attribute_value_list(FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
|
577
641
|
})
|
578
642
|
end
|
579
643
|
end
|
@@ -661,7 +725,7 @@ module Dynamoid
|
|
661
725
|
check = {again: true}
|
662
726
|
while check[:again]
|
663
727
|
sleep Dynamoid::Config.sync_retry_wait_seconds
|
664
|
-
resp = client.describe_table(
|
728
|
+
resp = client.describe_table(table_name: table_name)
|
665
729
|
check = check_table_status?(counter, resp, status)
|
666
730
|
Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
|
667
731
|
counter += 1
|
@@ -689,7 +753,7 @@ module Dynamoid
|
|
689
753
|
end
|
690
754
|
end
|
691
755
|
|
692
|
-
#Converts from symbol to the API string for the given data type
|
756
|
+
# Converts from symbol to the API string for the given data type
|
693
757
|
# E.g. :number -> 'N'
|
694
758
|
def api_type(type)
|
695
759
|
case(type)
|
@@ -714,13 +778,17 @@ module Dynamoid
|
|
714
778
|
# @return an Expected stanza for the given conditions hash
|
715
779
|
#
|
716
780
|
def expected_stanza(conditions = nil)
|
717
|
-
expected = Hash.new { |h,k| h[k] = {} }
|
781
|
+
expected = Hash.new { |h, k| h[k] = {} }
|
718
782
|
return expected unless conditions
|
719
783
|
|
720
784
|
conditions.delete(:unless_exists).try(:each) do |col|
|
721
785
|
expected[col.to_s][:exists] = false
|
722
786
|
end
|
723
|
-
conditions.delete(:
|
787
|
+
conditions.delete(:if_exists).try(:each) do |col, val|
|
788
|
+
expected[col.to_s][:exists] = true
|
789
|
+
expected[col.to_s][:value] = val
|
790
|
+
end
|
791
|
+
conditions.delete(:if).try(:each) do |col, val|
|
724
792
|
expected[col.to_s][:value] = val
|
725
793
|
end
|
726
794
|
|
@@ -741,7 +809,7 @@ module Dynamoid
|
|
741
809
|
#
|
742
810
|
def result_item_to_hash(item)
|
743
811
|
{}.tap do |r|
|
744
|
-
item.each { |k,v| r[k.to_sym] = v }
|
812
|
+
item.each { |k, v| r[k.to_sym] = v }
|
745
813
|
end
|
746
814
|
end
|
747
815
|
|
@@ -770,10 +838,10 @@ module Dynamoid
|
|
770
838
|
key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
|
771
839
|
|
772
840
|
hash = {
|
773
|
-
:
|
774
|
-
:
|
775
|
-
:
|
776
|
-
:
|
841
|
+
index_name: index.name,
|
842
|
+
key_schema: key_schema,
|
843
|
+
projection: {
|
844
|
+
projection_type: index.projection_type.to_s.upcase
|
777
845
|
}
|
778
846
|
}
|
779
847
|
|
@@ -785,8 +853,8 @@ module Dynamoid
|
|
785
853
|
# Only global secondary indexes have a separate throughput.
|
786
854
|
if index.type == :global_secondary
|
787
855
|
hash[:provisioned_throughput] = {
|
788
|
-
:
|
789
|
-
:
|
856
|
+
read_capacity_units: index.read_capacity,
|
857
|
+
write_capacity_units: index.write_capacity
|
790
858
|
}
|
791
859
|
end
|
792
860
|
hash
|
@@ -856,7 +924,6 @@ module Dynamoid
|
|
856
924
|
attribute_definitions
|
857
925
|
end
|
858
926
|
|
859
|
-
|
860
927
|
# Builds an attribute definitions based on hash key and range key
|
861
928
|
# @params [Hash] hash_key_schema - eg: {:id => :string}
|
862
929
|
# @params [Hash] range_key_schema - eg: {:created_at => :datetime}
|
@@ -887,11 +954,26 @@ module Dynamoid
|
|
887
954
|
aws_type = api_type(dynamoid_type)
|
888
955
|
|
889
956
|
{
|
890
|
-
:
|
891
|
-
:
|
957
|
+
attribute_name: name.to_s,
|
958
|
+
attribute_type: aws_type
|
892
959
|
}
|
893
960
|
end
|
894
961
|
|
962
|
+
# Build an array of values for Condition
|
963
|
+
# Is used in ScanFilter and QueryFilter
|
964
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
965
|
+
# @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
|
966
|
+
# @params [Object] value: scalar value or array/set
|
967
|
+
def attribute_value_list(operator, value)
|
968
|
+
# For BETWEEN and IN operators we should keep value as is (it should be already an array)
|
969
|
+
# For all the other operators we wrap the value with array
|
970
|
+
if ["BETWEEN", "IN"].include?(operator)
|
971
|
+
[value].flatten
|
972
|
+
else
|
973
|
+
[value]
|
974
|
+
end
|
975
|
+
end
|
976
|
+
|
895
977
|
#
|
896
978
|
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
897
979
|
# provides methods for coercing values to the proper types based on the table's schema data
|
@@ -913,7 +995,7 @@ module Dynamoid
|
|
913
995
|
def range_type
|
914
996
|
range_type ||= schema[:attribute_definitions].find { |d|
|
915
997
|
d[:attribute_name] == range_key
|
916
|
-
}.try(:fetch
|
998
|
+
}.try(:fetch, :attribute_type, nil)
|
917
999
|
end
|
918
1000
|
|
919
1001
|
def hash_key
|
@@ -982,19 +1064,19 @@ module Dynamoid
|
|
982
1064
|
def to_h
|
983
1065
|
ret = {}
|
984
1066
|
|
985
|
-
@additions.each do |k,v|
|
1067
|
+
@additions.each do |k, v|
|
986
1068
|
ret[k.to_s] = {
|
987
1069
|
action: ADD,
|
988
1070
|
value: v
|
989
1071
|
}
|
990
1072
|
end
|
991
|
-
@deletions.each do |k,v|
|
1073
|
+
@deletions.each do |k, v|
|
992
1074
|
ret[k.to_s] = {
|
993
1075
|
action: DELETE,
|
994
1076
|
value: v
|
995
1077
|
}
|
996
1078
|
end
|
997
|
-
@updates.each do |k,v|
|
1079
|
+
@updates.each do |k, v|
|
998
1080
|
ret[k.to_s] = {
|
999
1081
|
action: PUT,
|
1000
1082
|
value: v
|
@@ -1004,9 +1086,15 @@ module Dynamoid
|
|
1004
1086
|
ret
|
1005
1087
|
end
|
1006
1088
|
|
1007
|
-
ADD =
|
1008
|
-
DELETE =
|
1009
|
-
PUT =
|
1089
|
+
ADD = 'ADD'.freeze
|
1090
|
+
DELETE = 'DELETE'.freeze
|
1091
|
+
PUT = 'PUT'.freeze
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
def sanitize_item(attributes)
|
1095
|
+
attributes.reject do |k, v|
|
1096
|
+
v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
1097
|
+
end
|
1010
1098
|
end
|
1011
1099
|
end
|
1012
1100
|
end
|