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