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
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
@@ -18,7 +18,7 @@ Vagrant.configure('2') do |config|
18
18
  # Pillars
19
19
  salt.pillar({
20
20
  'ruby' => {
21
- 'version' => '2.3.3',
21
+ 'version' => '2.4.1',
22
22
  }
23
23
  })
24
24
 
@@ -0,0 +1,7 @@
1
+ version: '2'
2
+
3
+ services:
4
+ dynamodb:
5
+ image: deangiberson/aws-dynamodb-local
6
+ ports:
7
+ - 8000:8000
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", "~> 0.10"
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(%q<coveralls>, [">= 0"])
61
- spec.add_development_dependency(%q<rspec-retry>, [">= 0"])
61
+ spec.add_development_dependency "coveralls", "~> 0.8"
62
62
  end
@@ -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
- # Still tested against Ruby 2.0.0, which can't install nokogiri 1.7+
7
- gem "nokogiri", "~> 1.6.8.1"
7
+ gem "nokogiri", "~> 1.6.8"
8
8
 
9
- gemspec :path => "../"
9
+ gemspec path: "../"
@@ -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
- # Still tested against Ruby 2.0.0, which can't install nokogiri 1.7+
7
- gem "nokogiri", "~> 1.6.8.1"
7
+ gem "nokogiri", "~> 1.6.8"
8
8
 
9
- gemspec :path => "../"
9
+ gemspec path: "../"
@@ -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
- # Still tested against Ruby 2.0.0, which can't install nokogiri 1.7+
7
- gem "nokogiri", "~> 1.6.8.1"
7
+ gem "nokogiri", "~> 1.6.8"
8
8
 
9
- gemspec :path => "../"
9
+ gemspec path: "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
+ gem "pry-byebug", platforms: :ruby
5
6
  gem "rails", "~> 5.0.0"
6
7
 
7
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry-byebug", platforms: :ruby
6
+ gem "rails", "~> 5.1.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry-byebug", platforms: :ruby
6
+ gem "rails", "~> 5.2.0"
7
+
8
+ gemspec path: "../"
data/lib/dynamoid.rb CHANGED
@@ -1,36 +1,36 @@
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"
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 "dynamoid/railtie"
33
+ require 'dynamoid/railtie'
34
34
  end
35
35
 
36
36
  module Dynamoid
@@ -51,7 +51,7 @@ module Dynamoid
51
51
  def benchmark(method, *args)
52
52
  start = Time.now
53
53
  result = yield
54
- Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
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 ? [[ids, range_key]] : ids
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
- benchmark("#{m.to_s}", *args) {adapter.send(m, *args)}
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 = "EQ".freeze
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 = "HASH".freeze
32
- RANGE_KEY = "RANGE".freeze
33
- STRING_TYPE = "S".freeze
34
- NUM_TYPE = "N".freeze
35
- BINARY_TYPE = "B".freeze
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: "CREATING",
38
- updating: "UPDATING",
39
- deleting: "DELETING",
40
- active: "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 or deletes multiple items in one or more tables
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: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
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
- request_items = []
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
- client.batch_write_item(
100
- {
101
- request_items: {
102
- table_name => request_items,
103
- },
104
- return_consumed_capacity: "TOTAL",
105
- return_item_collection_metrics: "SIZE"
106
- }.merge!(options)
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({'table1' => ['1', '2']})
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 http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
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
- Array(ids).each_slice(Dynamoid::Config.batch_size) do |ids|
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(ids).map do |h,r|
193
+ Array(batch).map do |h, r|
143
194
  { hk => h, rng => r }
144
195
  end
145
196
  else
146
- Array(ids).map do |id|
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
- results.data[:responses].each do |table, rows|
160
- ret[table] += rows.collect { |r| result_item_to_hash(r) }
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
- ret
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
- # @return nil
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
- # @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
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
- ids.each do |id|
184
- client.delete_item(table_name: table_name, key: key_stanza(table, *id))
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
- nil
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
- :hash_key_schema => { key => (options[:hash_key_type] || :string) },
214
- :range_key_schema => options[:range_key]
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: "ALL_NEW"
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
- limit = [record_limit, scan_limit, batch_size].compact.min
456
- q[:limit] = limit if limit
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].flatten
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({ table_name: table_name })
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(:if).try(:each) do |col,val|
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
- :index_name => index.name,
774
- :key_schema => key_schema,
775
- :projection => {
776
- :projection_type => index.projection_type.to_s.upcase
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
- :read_capacity_units => index.read_capacity,
789
- :write_capacity_units => index.write_capacity
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
- :attribute_name => name.to_s,
891
- :attribute_type => aws_type
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,:attribute_type, nil)
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 = "ADD".freeze
1008
- DELETE = "DELETE".freeze
1009
- PUT = "PUT".freeze
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