dynamoid 1.3.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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