dynamoid 1.3.4 → 2.0.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +32 -7
  5. data/Appraisals +7 -0
  6. data/CHANGELOG.md +69 -2
  7. data/Gemfile +2 -0
  8. data/README.md +108 -28
  9. data/Rakefile +0 -24
  10. data/docker-compose.yml +7 -0
  11. data/dynamoid.gemspec +2 -3
  12. data/gemfiles/rails_4_0.gemfile +2 -3
  13. data/gemfiles/rails_4_1.gemfile +2 -3
  14. data/gemfiles/rails_4_2.gemfile +2 -3
  15. data/gemfiles/rails_5_0.gemfile +1 -1
  16. data/gemfiles/rails_5_1.gemfile +7 -0
  17. data/lib/dynamoid.rb +31 -31
  18. data/lib/dynamoid/adapter.rb +5 -5
  19. data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +84 -57
  20. data/lib/dynamoid/associations.rb +21 -12
  21. data/lib/dynamoid/associations/association.rb +19 -3
  22. data/lib/dynamoid/associations/belongs_to.rb +26 -16
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -16
  24. data/lib/dynamoid/associations/has_many.rb +2 -17
  25. data/lib/dynamoid/associations/has_one.rb +0 -14
  26. data/lib/dynamoid/associations/many_association.rb +19 -6
  27. data/lib/dynamoid/associations/single_association.rb +25 -7
  28. data/lib/dynamoid/config.rb +18 -18
  29. data/lib/dynamoid/config/options.rb +1 -1
  30. data/lib/dynamoid/criteria/chain.rb +29 -21
  31. data/lib/dynamoid/dirty.rb +2 -2
  32. data/lib/dynamoid/document.rb +17 -5
  33. data/lib/dynamoid/errors.rb +4 -1
  34. data/lib/dynamoid/fields.rb +6 -6
  35. data/lib/dynamoid/finders.rb +19 -9
  36. data/lib/dynamoid/identity_map.rb +0 -1
  37. data/lib/dynamoid/indexes.rb +41 -54
  38. data/lib/dynamoid/persistence.rb +54 -24
  39. data/lib/dynamoid/railtie.rb +1 -1
  40. data/lib/dynamoid/validations.rb +4 -3
  41. data/lib/dynamoid/version.rb +1 -1
  42. metadata +14 -29
  43. data/gemfiles/rails_4_0.gemfile.lock +0 -150
  44. data/gemfiles/rails_4_1.gemfile.lock +0 -154
  45. data/gemfiles/rails_4_2.gemfile.lock +0 -175
  46. 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
@@ -0,0 +1,7 @@
1
+ version: '2'
2
+
3
+ services:
4
+ dynamodb:
5
+ image: deangiberson/aws-dynamodb-local
6
+ ports:
7
+ - 8000:8000
@@ -50,13 +50,12 @@ Gem::Specification.new do |spec|
50
50
  spec.add_development_dependency(%q<activesupport>, [">= 4"])
51
51
  spec.add_runtime_dependency(%q<aws-sdk-resources>, ["~> 2"])
52
52
  spec.add_runtime_dependency(%q<concurrent-ruby>, [">= 1.0"])
53
- spec.add_development_dependency "pry", "~> 0.10"
53
+ spec.add_development_dependency "pry"
54
54
  spec.add_development_dependency "bundler", "~> 1.14"
55
55
  spec.add_development_dependency "rake", "~> 12.0"
56
56
  spec.add_development_dependency "rspec", "~> 3.0"
57
57
  spec.add_development_dependency "appraisal", "~> 2.1"
58
58
  spec.add_development_dependency "wwtd", "~> 1.3"
59
59
  spec.add_development_dependency(%q<yard>, [">= 0"])
60
- spec.add_development_dependency(%q<coveralls>, [">= 0"])
61
- spec.add_development_dependency(%q<rspec-retry>, [">= 0"])
60
+ spec.add_development_dependency "coveralls", "~> 0.8"
62
61
  end
@@ -3,7 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  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"
6
+ gem "nokogiri", "~> 1.6.8"
8
7
 
9
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -3,7 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  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"
6
+ gem "nokogiri", "~> 1.6.8"
8
7
 
9
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -3,7 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  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"
6
+ gem "nokogiri", "~> 1.6.8"
8
7
 
9
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -4,4 +4,4 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 5.0.0"
6
6
 
7
- gemspec :path => "../"
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1.0"
6
+
7
+ gemspec path: "../"
@@ -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
@@ -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
 
@@ -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',
@@ -28,16 +28,16 @@ module Dynamoid
28
28
  contains: 'CONTAINS',
29
29
  not_contains: 'NOT_CONTAINS'
30
30
  }
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
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
36
36
  TABLE_STATUSES = {
37
- creating: "CREATING",
38
- updating: "UPDATING",
39
- deleting: "DELETING",
40
- active: "ACTIVE"
37
+ creating: 'CREATING',
38
+ updating: 'UPDATING',
39
+ deleting: 'DELETING',
40
+ active: 'ACTIVE'
41
41
  }.freeze
42
42
  PARSE_TABLE_STATUS = ->(resp, lookup = :table) {
43
43
  # lookup is table for describe_table API
@@ -45,6 +45,8 @@ module Dynamoid
45
45
  # because Amazon, damnit.
46
46
  resp.send(lookup).table_status
47
47
  }
48
+ BATCH_WRITE_ITEM_REQUESTS_LIMIT = 25
49
+
48
50
  attr_reader :table_cache
49
51
 
50
52
  # Establish the connection to DynamoDB.
@@ -87,24 +89,31 @@ module Dynamoid
87
89
  # @param [Array] items to be processed
88
90
  # @param [Hash] additional options
89
91
  #
90
- #See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
92
+ # See:
93
+ # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
94
+ # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
95
+ #
96
+ # TODO handle rejections because of exceeding limit for the whole request - 16 MB,
97
+ # item size limit - 400 KB or because provisioned throughput is exceeded
91
98
  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 } }
99
+ requests = []
100
+
101
+ objects.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |os|
102
+ requests << os.map { |o| { put_request: { item: o } } }
96
103
  end
97
104
 
98
105
  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
- )
106
+ requests.each do |request_items|
107
+ client.batch_write_item(
108
+ {
109
+ request_items: {
110
+ table_name => request_items,
111
+ },
112
+ return_consumed_capacity: 'TOTAL',
113
+ return_item_collection_metrics: 'SIZE'
114
+ }.merge!(options)
115
+ )
116
+ end
108
117
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
109
118
  raise Dynamoid::Errors::ConditionalCheckFailedException, e
110
119
  end
@@ -139,7 +148,7 @@ module Dynamoid
139
148
  request_items = Hash.new{|h, k| h[k] = []}
140
149
 
141
150
  keys = if rng.present?
142
- Array(ids).map do |h,r|
151
+ Array(ids).map do |h, r|
143
152
  { hk => h, rng => r }
144
153
  end
145
154
  else
@@ -169,22 +178,41 @@ module Dynamoid
169
178
  #
170
179
  # @example Delete IDs 1 and 2 from the table testtable
171
180
  # Dynamoid::AdapterPlugin::AwsSdk.batch_delete_item('table1' => ['1', '2'])
172
- #or
181
+ # or
173
182
  # Dynamoid::AdapterPlugin::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
174
183
  #
175
184
  # @param [Hash] options the hash of tables and IDs to delete
176
185
  #
177
- # @return nil
186
+ # See:
187
+ # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
188
+ # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
178
189
  #
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
190
+ # TODO handle rejections because of internal processing failures
180
191
  def batch_delete_item(options)
192
+ requests = []
193
+
181
194
  options.each_pair do |table_name, ids|
182
195
  table = describe_table(table_name)
183
- ids.each do |id|
184
- client.delete_item(table_name: table_name, key: key_stanza(table, *id))
196
+
197
+ ids.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |sliced_ids|
198
+ delete_requests = sliced_ids.map { |id|
199
+ {delete_request: {key: key_stanza(table, *id)}}
200
+ }
201
+
202
+ requests << {table_name => delete_requests}
203
+ end
204
+ end
205
+
206
+ begin
207
+ requests.map do |request_items|
208
+ client.batch_write_item(
209
+ request_items: request_items,
210
+ return_consumed_capacity: 'TOTAL',
211
+ return_item_collection_metrics: 'SIZE')
185
212
  end
213
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
214
+ raise Dynamoid::Errors::ConditionalCheckFailedException, e
186
215
  end
187
- nil
188
216
  end
189
217
 
190
218
  # Create a table on DynamoDB. This usually takes a long time to complete.
@@ -210,8 +238,8 @@ module Dynamoid
210
238
  gs_indexes = options[:global_secondary_indexes]
211
239
 
212
240
  key_schema = {
213
- :hash_key_schema => { key => (options[:hash_key_type] || :string) },
214
- :range_key_schema => options[:range_key]
241
+ hash_key_schema: { key => (options[:hash_key_type] || :string) },
242
+ range_key_schema: options[:range_key]
215
243
  }
216
244
  attribute_definitions = build_all_attribute_definitions(
217
245
  key_schema,
@@ -367,7 +395,7 @@ module Dynamoid
367
395
  key: key_stanza(table, key, range_key),
368
396
  attribute_updates: iu.to_h,
369
397
  expected: expected_stanza(conditions),
370
- return_values: "ALL_NEW"
398
+ return_values: 'ALL_NEW'
371
399
  )
372
400
  result_item_to_hash(result[:attributes])
373
401
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
@@ -489,7 +517,7 @@ module Dynamoid
489
517
  end
490
518
 
491
519
  query_filter = {}
492
- opts.reject {|k,_| k.in? RANGE_MAP.keys}.each do |attr, hash|
520
+ opts.reject {|k, _| k.in? RANGE_MAP.keys}.each do |attr, hash|
493
521
  query_filter[attr] = {
494
522
  comparison_operator: FIELD_MAP[hash.keys[0]],
495
523
  attribute_value_list: [
@@ -557,7 +585,7 @@ module Dynamoid
557
585
  # @since 1.0.0
558
586
  #
559
587
  # @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 = {})
588
+ def scan(table_name, scan_hash = {}, select_opts = {})
561
589
  request = { table_name: table_name }
562
590
  request[:consistent_read] = true if select_opts.delete(:consistent_read)
563
591
 
@@ -661,7 +689,7 @@ module Dynamoid
661
689
  check = {again: true}
662
690
  while check[:again]
663
691
  sleep Dynamoid::Config.sync_retry_wait_seconds
664
- resp = client.describe_table({ table_name: table_name })
692
+ resp = client.describe_table(table_name: table_name)
665
693
  check = check_table_status?(counter, resp, status)
666
694
  Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
667
695
  counter += 1
@@ -689,7 +717,7 @@ module Dynamoid
689
717
  end
690
718
  end
691
719
 
692
- #Converts from symbol to the API string for the given data type
720
+ # Converts from symbol to the API string for the given data type
693
721
  # E.g. :number -> 'N'
694
722
  def api_type(type)
695
723
  case(type)
@@ -714,13 +742,13 @@ module Dynamoid
714
742
  # @return an Expected stanza for the given conditions hash
715
743
  #
716
744
  def expected_stanza(conditions = nil)
717
- expected = Hash.new { |h,k| h[k] = {} }
745
+ expected = Hash.new { |h, k| h[k] = {} }
718
746
  return expected unless conditions
719
747
 
720
748
  conditions.delete(:unless_exists).try(:each) do |col|
721
749
  expected[col.to_s][:exists] = false
722
750
  end
723
- conditions.delete(:if).try(:each) do |col,val|
751
+ conditions.delete(:if).try(:each) do |col, val|
724
752
  expected[col.to_s][:value] = val
725
753
  end
726
754
 
@@ -741,7 +769,7 @@ module Dynamoid
741
769
  #
742
770
  def result_item_to_hash(item)
743
771
  {}.tap do |r|
744
- item.each { |k,v| r[k.to_sym] = v }
772
+ item.each { |k, v| r[k.to_sym] = v }
745
773
  end
746
774
  end
747
775
 
@@ -770,10 +798,10 @@ module Dynamoid
770
798
  key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
771
799
 
772
800
  hash = {
773
- :index_name => index.name,
774
- :key_schema => key_schema,
775
- :projection => {
776
- :projection_type => index.projection_type.to_s.upcase
801
+ index_name: index.name,
802
+ key_schema: key_schema,
803
+ projection: {
804
+ projection_type: index.projection_type.to_s.upcase
777
805
  }
778
806
  }
779
807
 
@@ -785,8 +813,8 @@ module Dynamoid
785
813
  # Only global secondary indexes have a separate throughput.
786
814
  if index.type == :global_secondary
787
815
  hash[:provisioned_throughput] = {
788
- :read_capacity_units => index.read_capacity,
789
- :write_capacity_units => index.write_capacity
816
+ read_capacity_units: index.read_capacity,
817
+ write_capacity_units: index.write_capacity
790
818
  }
791
819
  end
792
820
  hash
@@ -856,7 +884,6 @@ module Dynamoid
856
884
  attribute_definitions
857
885
  end
858
886
 
859
-
860
887
  # Builds an attribute definitions based on hash key and range key
861
888
  # @params [Hash] hash_key_schema - eg: {:id => :string}
862
889
  # @params [Hash] range_key_schema - eg: {:created_at => :datetime}
@@ -887,8 +914,8 @@ module Dynamoid
887
914
  aws_type = api_type(dynamoid_type)
888
915
 
889
916
  {
890
- :attribute_name => name.to_s,
891
- :attribute_type => aws_type
917
+ attribute_name: name.to_s,
918
+ attribute_type: aws_type
892
919
  }
893
920
  end
894
921
 
@@ -913,7 +940,7 @@ module Dynamoid
913
940
  def range_type
914
941
  range_type ||= schema[:attribute_definitions].find { |d|
915
942
  d[:attribute_name] == range_key
916
- }.try(:fetch,:attribute_type, nil)
943
+ }.try(:fetch, :attribute_type, nil)
917
944
  end
918
945
 
919
946
  def hash_key
@@ -982,19 +1009,19 @@ module Dynamoid
982
1009
  def to_h
983
1010
  ret = {}
984
1011
 
985
- @additions.each do |k,v|
1012
+ @additions.each do |k, v|
986
1013
  ret[k.to_s] = {
987
1014
  action: ADD,
988
1015
  value: v
989
1016
  }
990
1017
  end
991
- @deletions.each do |k,v|
1018
+ @deletions.each do |k, v|
992
1019
  ret[k.to_s] = {
993
1020
  action: DELETE,
994
1021
  value: v
995
1022
  }
996
1023
  end
997
- @updates.each do |k,v|
1024
+ @updates.each do |k, v|
998
1025
  ret[k.to_s] = {
999
1026
  action: PUT,
1000
1027
  value: v
@@ -1004,9 +1031,9 @@ module Dynamoid
1004
1031
  ret
1005
1032
  end
1006
1033
 
1007
- ADD = "ADD".freeze
1008
- DELETE = "DELETE".freeze
1009
- PUT = "PUT".freeze
1034
+ ADD = 'ADD'.freeze
1035
+ DELETE = 'DELETE'.freeze
1036
+ PUT = 'PUT'.freeze
1010
1037
  end
1011
1038
  end
1012
1039
  end