dynamoid 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +18 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +113 -63
  6. data/Vagrantfile +2 -2
  7. data/docker-compose.yml +1 -1
  8. data/gemfiles/rails_4_2.gemfile +1 -1
  9. data/gemfiles/rails_5_0.gemfile +1 -1
  10. data/gemfiles/rails_5_1.gemfile +1 -1
  11. data/gemfiles/rails_5_2.gemfile +1 -1
  12. data/lib/dynamoid/adapter.rb +1 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
  17. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
  18. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
  19. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
  20. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
  21. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
  24. data/lib/dynamoid/associations/has_many.rb +1 -0
  25. data/lib/dynamoid/associations/has_one.rb +1 -0
  26. data/lib/dynamoid/associations/single_association.rb +1 -0
  27. data/lib/dynamoid/criteria.rb +4 -4
  28. data/lib/dynamoid/criteria/chain.rb +86 -79
  29. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
  30. data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
  31. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
  32. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
  33. data/lib/dynamoid/document.rb +18 -13
  34. data/lib/dynamoid/dumping.rb +52 -40
  35. data/lib/dynamoid/fields.rb +4 -3
  36. data/lib/dynamoid/finders.rb +3 -3
  37. data/lib/dynamoid/persistence.rb +5 -6
  38. data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
  39. data/lib/dynamoid/tasks.rb +1 -0
  40. data/lib/dynamoid/tasks/database.rake +2 -2
  41. data/lib/dynamoid/type_casting.rb +37 -19
  42. data/lib/dynamoid/undumping.rb +53 -42
  43. data/lib/dynamoid/validations.rb +2 -0
  44. data/lib/dynamoid/version.rb +1 -1
  45. metadata +17 -5
  46. data/lib/dynamoid/adapter_plugin/query.rb +0 -144
  47. data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'until_past_table_status'
4
+
5
+ module Dynamoid
6
+ module AdapterPlugin
7
+ class AwsSdkV3
8
+ class CreateTable
9
+ attr_reader :client, :table_name, :key, :options
10
+
11
+ def initialize(client, table_name, key, options)
12
+ @client = client
13
+ @table_name = table_name
14
+ @key = key
15
+ @options = options
16
+ end
17
+
18
+ def call
19
+ read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
20
+ write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
21
+
22
+ secondary_indexes = options.slice(
23
+ :local_secondary_indexes,
24
+ :global_secondary_indexes
25
+ )
26
+ ls_indexes = options[:local_secondary_indexes]
27
+ gs_indexes = options[:global_secondary_indexes]
28
+
29
+ key_schema = {
30
+ hash_key_schema: { key => (options[:hash_key_type] || :string) },
31
+ range_key_schema: options[:range_key]
32
+ }
33
+ attribute_definitions = build_all_attribute_definitions(
34
+ key_schema,
35
+ secondary_indexes
36
+ )
37
+ key_schema = aws_key_schema(
38
+ key_schema[:hash_key_schema],
39
+ key_schema[:range_key_schema]
40
+ )
41
+
42
+ client_opts = {
43
+ table_name: table_name,
44
+ provisioned_throughput: {
45
+ read_capacity_units: read_capacity,
46
+ write_capacity_units: write_capacity
47
+ },
48
+ key_schema: key_schema,
49
+ attribute_definitions: attribute_definitions
50
+ }
51
+
52
+ if ls_indexes.present?
53
+ client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
54
+ index_to_aws_hash(index)
55
+ end
56
+ end
57
+
58
+ if gs_indexes.present?
59
+ client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
60
+ index_to_aws_hash(index)
61
+ end
62
+ end
63
+ resp = client.create_table(client_opts)
64
+ options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
65
+ UntilPastTableStatus.new(table_name, :creating).call if options[:sync] &&
66
+ (status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
67
+ status == TABLE_STATUSES[:creating]
68
+ # Response to original create_table, which, if options[:sync]
69
+ # may have an outdated table_description.table_status of "CREATING"
70
+ resp
71
+ end
72
+
73
+ private
74
+
75
+ # Builds aws attributes definitions based off of primary hash/range and
76
+ # secondary indexes
77
+ #
78
+ # @param key_data
79
+ # @option key_data [Hash] hash_key_schema - eg: {:id => :string}
80
+ # @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
81
+ # @param [Hash] secondary_indexes
82
+ # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
83
+ # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
84
+ def build_all_attribute_definitions(key_schema, secondary_indexes = {})
85
+ ls_indexes = secondary_indexes[:local_secondary_indexes]
86
+ gs_indexes = secondary_indexes[:global_secondary_indexes]
87
+
88
+ attribute_definitions = []
89
+
90
+ attribute_definitions << build_attribute_definitions(
91
+ key_schema[:hash_key_schema],
92
+ key_schema[:range_key_schema]
93
+ )
94
+
95
+ if ls_indexes.present?
96
+ ls_indexes.map do |index|
97
+ attribute_definitions << build_attribute_definitions(
98
+ index.hash_key_schema,
99
+ index.range_key_schema
100
+ )
101
+ end
102
+ end
103
+
104
+ if gs_indexes.present?
105
+ gs_indexes.map do |index|
106
+ attribute_definitions << build_attribute_definitions(
107
+ index.hash_key_schema,
108
+ index.range_key_schema
109
+ )
110
+ end
111
+ end
112
+
113
+ attribute_definitions.flatten!
114
+ # uniq these definitions because range keys might be common between
115
+ # primary and secondary indexes
116
+ attribute_definitions.uniq!
117
+ attribute_definitions
118
+ end
119
+
120
+ # Builds an attribute definitions based on hash key and range key
121
+ # @params [Hash] hash_key_schema - eg: {:id => :string}
122
+ # @params [Hash] range_key_schema - eg: {:created_at => :datetime}
123
+ # @return [Array]
124
+ def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
125
+ attrs = []
126
+
127
+ attrs << attribute_definition_element(
128
+ hash_key_schema.keys.first,
129
+ hash_key_schema.values.first
130
+ )
131
+
132
+ if range_key_schema.present?
133
+ attrs << attribute_definition_element(
134
+ range_key_schema.keys.first,
135
+ range_key_schema.values.first
136
+ )
137
+ end
138
+
139
+ attrs
140
+ end
141
+
142
+ # Builds an aws attribute definition based on name and dynamoid type
143
+ # @params [Symbol] name - eg: :id
144
+ # @params [Symbol] dynamoid_type - eg: :string
145
+ # @return [Hash]
146
+ def attribute_definition_element(name, dynamoid_type)
147
+ aws_type = api_type(dynamoid_type)
148
+
149
+ {
150
+ attribute_name: name.to_s,
151
+ attribute_type: aws_type
152
+ }
153
+ end
154
+
155
+ # Converts from symbol to the API string for the given data type
156
+ # E.g. :number -> 'N'
157
+ def api_type(type)
158
+ case type
159
+ when :string then STRING_TYPE
160
+ when :number then NUM_TYPE
161
+ when :binary then BINARY_TYPE
162
+ else raise "Unknown type: #{type}"
163
+ end
164
+ end
165
+
166
+ # Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
167
+ # This resulting hash is of the form:
168
+ #
169
+ # {
170
+ # index_name: String
171
+ # keys: {
172
+ # hash_key: aws_key_schema (hash)
173
+ # range_key: aws_key_schema (hash)
174
+ # }
175
+ # projection: {
176
+ # projection_type: (ALL, KEYS_ONLY, INCLUDE) String
177
+ # non_key_attributes: (optional) Array
178
+ # }
179
+ # provisioned_throughput: {
180
+ # read_capacity_units: Integer
181
+ # write_capacity_units: Integer
182
+ # }
183
+ # }
184
+ #
185
+ # @param [Dynamoid::Indexes::Index] index the index.
186
+ # @return [Hash] hash representing an AWS Index definition.
187
+ def index_to_aws_hash(index)
188
+ key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
189
+
190
+ hash = {
191
+ index_name: index.name,
192
+ key_schema: key_schema,
193
+ projection: {
194
+ projection_type: index.projection_type.to_s.upcase
195
+ }
196
+ }
197
+
198
+ # If the projection type is include, specify the non key attributes
199
+ if index.projection_type == :include
200
+ hash[:projection][:non_key_attributes] = index.projected_attributes
201
+ end
202
+
203
+ # Only global secondary indexes have a separate throughput.
204
+ if index.type == :global_secondary
205
+ hash[:provisioned_throughput] = {
206
+ read_capacity_units: index.read_capacity,
207
+ write_capacity_units: index.write_capacity
208
+ }
209
+ end
210
+ hash
211
+ end
212
+
213
+ # Converts hash_key_schema and range_key_schema to aws_key_schema
214
+ # @param [Hash] hash_key_schema eg: {:id => :string}
215
+ # @param [Hash] range_key_schema eg: {:created_at => :number}
216
+ # @return [Array]
217
+ def aws_key_schema(hash_key_schema, range_key_schema)
218
+ schema = [{
219
+ attribute_name: hash_key_schema.keys.first.to_s,
220
+ key_type: HASH_KEY
221
+ }]
222
+
223
+ if range_key_schema.present?
224
+ schema << {
225
+ attribute_name: range_key_schema.keys.first.to_s,
226
+ key_type: RANGE_KEY
227
+ }
228
+ end
229
+ schema
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ # Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
7
+ class ItemUpdater
8
+ attr_reader :table, :key, :range_key
9
+
10
+ def initialize(table, key, range_key = nil)
11
+ @table = table
12
+ @key = key
13
+ @range_key = range_key
14
+ @additions = {}
15
+ @deletions = {}
16
+ @updates = {}
17
+ end
18
+
19
+ #
20
+ # Adds the given values to the values already stored in the corresponding columns.
21
+ # The column must contain a Set or a number.
22
+ #
23
+ # @param [Hash] vals keys of the hash are the columns to update, vals are the values to
24
+ # add. values must be a Set, Array, or Numeric
25
+ #
26
+ def add(values)
27
+ @additions.merge!(sanitize_attributes(values))
28
+ end
29
+
30
+ #
31
+ # Removes values from the sets of the given columns
32
+ #
33
+ # @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
34
+ # to remove
35
+ #
36
+ def delete(values)
37
+ @deletions.merge!(sanitize_attributes(values))
38
+ end
39
+
40
+ #
41
+ # Replaces the values of one or more attributes
42
+ #
43
+ def set(values)
44
+ @updates.merge!(sanitize_attributes(values))
45
+ end
46
+
47
+ #
48
+ # Returns an AttributeUpdates hash suitable for passing to the V2 Client API
49
+ #
50
+ def to_h
51
+ ret = {}
52
+
53
+ @additions.each do |k, v|
54
+ ret[k.to_s] = {
55
+ action: ADD,
56
+ value: v
57
+ }
58
+ end
59
+ @deletions.each do |k, v|
60
+ ret[k.to_s] = {
61
+ action: DELETE,
62
+ value: v
63
+ }
64
+ end
65
+ @updates.each do |k, v|
66
+ ret[k.to_s] = {
67
+ action: PUT,
68
+ value: v
69
+ }
70
+ end
71
+
72
+ ret
73
+ end
74
+
75
+ private
76
+
77
+ def sanitize_attributes(attributes)
78
+ attributes.transform_values do |v|
79
+ v.is_a?(Hash) ? v.stringify_keys : v
80
+ end
81
+ end
82
+
83
+ ADD = 'ADD'
84
+ DELETE = 'DELETE'
85
+ PUT = 'PUT'
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ module Middleware
7
+ class Backoff
8
+ def initialize(next_chain)
9
+ @next_chain = next_chain
10
+ @backoff = Dynamoid.config.backoff ? Dynamoid.config.build_backoff : nil
11
+ end
12
+
13
+ def call(request)
14
+ response = @next_chain.call(request)
15
+ @backoff.call if @backoff
16
+
17
+ return response
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ module Middleware
7
+ class Limit
8
+ def initialize(next_chain, record_limit: nil, scan_limit: nil)
9
+ @next_chain = next_chain
10
+
11
+ @record_limit = record_limit
12
+ @scan_limit = scan_limit
13
+
14
+ @record_count = 0
15
+ @scan_count = 0
16
+ end
17
+
18
+ def call(request)
19
+ # Adjust the limit down if the remaining record and/or scan limit are
20
+ # lower to obey limits. We can assume the difference won't be
21
+ # negative due to break statements below but choose smaller limit
22
+ # which is why we have 2 separate if statements.
23
+ # NOTE: Adjusting based on record_limit can cause many HTTP requests
24
+ # being made. We may want to change this behavior, but it affects
25
+ # filtering on data with potentially large gaps.
26
+ # Example:
27
+ # User.where('created_at.gte' => 1.day.ago).record_limit(1000)
28
+ # Records 1-999 User's that fit criteria
29
+ # Records 1000-2000 Users's that do not fit criteria
30
+ # Record 2001 fits criteria
31
+ # The underlying implementation will have 1 page for records 1-999
32
+ # then will request with limit 1 for records 1000-2000 (making 1000
33
+ # requests of limit 1) until hit record 2001.
34
+ if request[:limit] && @record_limit && @record_limit - @record_count < request[:limit]
35
+ request[:limit] = @record_limit - @record_count
36
+ end
37
+ if request[:limit] && @scan_limit && @scan_limit - @scan_count < request[:limit]
38
+ request[:limit] = @scan_limit - @scan_count
39
+ end
40
+
41
+ response = @next_chain.call(request)
42
+
43
+ @record_count += response.count
44
+ throw :stop_pagination if @record_limit && @record_count >= @record_limit
45
+
46
+ @scan_count += response.scanned_count
47
+ throw :stop_pagination if @scan_limit && @scan_count >= @scan_limit
48
+
49
+ return response
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ module Middleware
7
+ class StartKey
8
+ def initialize(next_chain)
9
+ @next_chain = next_chain
10
+ end
11
+
12
+ def call(request)
13
+ response = @next_chain.call(request)
14
+
15
+ if response.last_evaluated_key
16
+ request[:exclusive_start_key] = response.last_evaluated_key
17
+ else
18
+ throw :stop_pagination
19
+ end
20
+
21
+ return response
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middleware/backoff'
4
+ require_relative 'middleware/limit'
5
+ require_relative 'middleware/start_key'
6
+
7
+ module Dynamoid
8
+ module AdapterPlugin
9
+ class AwsSdkV3
10
+ class Query
11
+ OPTIONS_KEYS = %i[
12
+ limit hash_key hash_value range_key consistent_read scan_index_forward
13
+ select index_name batch_size exclusive_start_key record_limit scan_limit
14
+ ].freeze
15
+
16
+ attr_reader :client, :table, :options, :conditions
17
+
18
+ def initialize(client, table, opts = {})
19
+ @client = client
20
+ @table = table
21
+
22
+ opts = opts.symbolize_keys
23
+ @options = opts.slice(*OPTIONS_KEYS)
24
+ @conditions = opts.except(*OPTIONS_KEYS)
25
+ end
26
+
27
+ def call
28
+ request = build_request
29
+
30
+ Enumerator.new do |yielder|
31
+ api_call = -> (request) do
32
+ client.query(request).tap do |response|
33
+ yielder << response
34
+ end
35
+ end
36
+
37
+ middlewares = Middleware::Backoff.new(
38
+ Middleware::StartKey.new(
39
+ Middleware::Limit.new(api_call, record_limit: record_limit, scan_limit: scan_limit)
40
+ )
41
+ )
42
+
43
+ catch :stop_pagination do
44
+ loop do
45
+ middlewares.call(request)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def build_request
54
+ request = options.slice(
55
+ :consistent_read,
56
+ :scan_index_forward,
57
+ :select,
58
+ :index_name,
59
+ :exclusive_start_key
60
+ ).compact
61
+
62
+ # Deal with various limits and batching
63
+ batch_size = options[:batch_size]
64
+ limit = [record_limit, scan_limit, batch_size].compact.min
65
+
66
+ request[:limit] = limit if limit
67
+ request[:table_name] = table.name
68
+ request[:key_conditions] = key_conditions
69
+ request[:query_filter] = query_filter
70
+
71
+ request
72
+ end
73
+
74
+ def record_limit
75
+ options[:record_limit]
76
+ end
77
+
78
+ def scan_limit
79
+ options[:scan_limit]
80
+ end
81
+
82
+ def hash_key_name
83
+ (options[:hash_key] || table.hash_key)
84
+ end
85
+
86
+ def range_key_name
87
+ (options[:range_key] || table.range_key)
88
+ end
89
+
90
+ def key_conditions
91
+ result = {
92
+ hash_key_name => {
93
+ comparison_operator: AwsSdkV3::EQ,
94
+ attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::EQ, options[:hash_value].freeze)
95
+ }
96
+ }
97
+
98
+ conditions.slice(*AwsSdkV3::RANGE_MAP.keys).each do |k, _v|
99
+ op = AwsSdkV3::RANGE_MAP[k]
100
+
101
+ result[range_key_name] = {
102
+ comparison_operator: op,
103
+ attribute_value_list: AwsSdkV3.attribute_value_list(op, conditions[k].freeze)
104
+ }
105
+ end
106
+
107
+ result
108
+ end
109
+
110
+ def query_filter
111
+ conditions.except(*AwsSdkV3::RANGE_MAP.keys).reduce({}) do |result, (attr, cond)|
112
+ condition = {
113
+ comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
114
+ attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
115
+ }
116
+ result[attr] = condition
117
+ result
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end