dynamoid 3.1.0 → 3.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 (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