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
@@ -43,7 +43,7 @@ module Dynamoid #:nodoc
43
43
  def #{name}?
44
44
  #{name}
45
45
  end
46
-
46
+
47
47
  def reset_#{name}
48
48
  settings[#{name.inspect}] = defaults[#{name.inspect}]
49
49
  end
@@ -18,6 +18,11 @@ module Dynamoid #:nodoc:
18
18
  @source = source
19
19
  @consistent_read = false
20
20
  @scan_index_forward = true
21
+
22
+ # Honor STI and :type field if it presents
23
+ if @source.attributes.key?(:type)
24
+ @query[:'type.in'] = @source.deep_subclasses.map(&:name) << @source.name
25
+ end
21
26
  end
22
27
 
23
28
  # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
@@ -56,32 +61,36 @@ module Dynamoid #:nodoc:
56
61
  end
57
62
 
58
63
  # Returns the last fetched record matched the criteria
64
+ # Enumerable doesn't implement `last`, only `first`
65
+ # So we have to implement it ourselves
59
66
  #
60
67
  def last
61
- all.last
68
+ all.to_a.last
62
69
  end
63
70
 
64
71
  # Destroys all the records matching the criteria.
65
72
  #
66
- def destroy_all
73
+ def delete_all
67
74
  ids = []
75
+ ranges = []
68
76
 
69
77
  if key_present?
70
- ranges = []
71
78
  Dynamoid.adapter.query(source.table_name, range_query).collect do |hash|
72
79
  ids << hash[source.hash_key.to_sym]
73
- ranges << hash[source.range_key.to_sym]
80
+ ranges << hash[source.range_key.to_sym] if source.range_key
74
81
  end
75
82
 
76
- Dynamoid.adapter.delete(source.table_name, ids,{:range_key => ranges})
83
+ Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence)
77
84
  else
78
- Dynamoid.adapter.scan(source.table_name, query, scan_opts).collect do |hash|
85
+ Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).collect do |hash|
79
86
  ids << hash[source.hash_key.to_sym]
87
+ ranges << hash[source.range_key.to_sym] if source.range_key
80
88
  end
81
89
 
82
- Dynamoid.adapter.delete(source.table_name, ids)
90
+ Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence)
83
91
  end
84
92
  end
93
+ alias_method :destroy_all, :delete_all
85
94
 
86
95
  # The record limit is the limit of evaluated records returned by the
87
96
  # query or scan.
@@ -121,10 +130,6 @@ module Dynamoid #:nodoc:
121
130
  records.each(&block)
122
131
  end
123
132
 
124
- def consistent_opts
125
- { :consistent_read => consistent_read }
126
- end
127
-
128
133
  private
129
134
 
130
135
  # The actual records referenced by the association.
@@ -133,12 +138,11 @@ module Dynamoid #:nodoc:
133
138
  #
134
139
  # @since 0.2.0
135
140
  def records
136
- results = if key_present?
141
+ if key_present?
137
142
  records_via_query
138
143
  else
139
144
  records_via_scan
140
145
  end
141
- @batch_size ? results : Array(results)
142
146
  end
143
147
 
144
148
  def records_via_query
@@ -173,17 +177,17 @@ module Dynamoid #:nodoc:
173
177
 
174
178
  case operation
175
179
  when 'gt'
176
- { :range_greater_than => val }
180
+ { range_greater_than: val }
177
181
  when 'lt'
178
- { :range_less_than => val }
182
+ { range_less_than: val }
179
183
  when 'gte'
180
- { :range_gte => val }
184
+ { range_gte: val }
181
185
  when 'lte'
182
- { :range_lte => val }
186
+ { range_lte: val }
183
187
  when 'between'
184
- { :range_between => val }
188
+ { range_between: val }
185
189
  when 'begins_with'
186
- { :range_begins_with => val }
190
+ { range_begins_with: val }
187
191
  end
188
192
  end
189
193
 
@@ -215,6 +219,10 @@ module Dynamoid #:nodoc:
215
219
  return { name.to_sym => hash }
216
220
  end
217
221
 
222
+ def consistent_opts
223
+ { consistent_read: consistent_read }
224
+ end
225
+
218
226
  def range_query
219
227
  opts = {}
220
228
 
@@ -227,7 +235,7 @@ module Dynamoid #:nodoc:
227
235
  opts[:range_key] = @range_key
228
236
  if query[@range_key].present?
229
237
  value = type_cast_condition_parameter(@range_key, query[@range_key])
230
- opts.update(:range_eq => value)
238
+ opts.update(range_eq: value)
231
239
  end
232
240
 
233
241
  query.keys.select { |k| k.to_s =~ /^#{@range_key}\./ }.each do |key|
@@ -261,7 +269,7 @@ module Dynamoid #:nodoc:
261
269
  query_keys = query.keys.collect { |k| k.to_s.split('.').first }
262
270
 
263
271
  # See if querying based on table hash key
264
- if query_keys.include?(source.hash_key.to_s)
272
+ if query.keys.map(&:to_s).include?(source.hash_key.to_s)
265
273
  @hash_key = source.hash_key
266
274
 
267
275
  # Use table's default range key
@@ -15,7 +15,7 @@ module Dynamoid
15
15
 
16
16
  def update!(*)
17
17
  ret = super
18
- clear_changes #update! completely reloads all fields on the class, so any extant changes are wiped out
18
+ clear_changes # update! completely reloads all fields on the class, so any extant changes are wiped out
19
19
  ret
20
20
  end
21
21
 
@@ -26,7 +26,7 @@ module Dynamoid
26
26
  def clear_changes
27
27
  previous = changes
28
28
  (block_given? ? yield : true).tap do |result|
29
- unless result == false #failed validation; nil is OK.
29
+ unless result == false # failed validation; nil is OK.
30
30
  @previously_changed = previous
31
31
  changed_attributes.clear
32
32
  end
@@ -72,7 +72,11 @@ module Dynamoid #:nodoc:
72
72
  #
73
73
  # @since 0.2.0
74
74
  def create(attrs = {})
75
- build(attrs).tap(&:save)
75
+ if attrs.is_a?(Array)
76
+ attrs.map { |attr| create(attr) }
77
+ else
78
+ build(attrs).tap(&:save)
79
+ end
76
80
  end
77
81
 
78
82
  # Initialize a new object and immediately save it to the database. Raise an exception if persistence failed.
@@ -83,7 +87,11 @@ module Dynamoid #:nodoc:
83
87
  #
84
88
  # @since 0.2.0
85
89
  def create!(attrs = {})
86
- build(attrs).tap(&:save!)
90
+ if attrs.is_a?(Array)
91
+ attrs.map { |attr| create!(attr) }
92
+ else
93
+ build(attrs).tap(&:save!)
94
+ end
87
95
  end
88
96
 
89
97
  # Initialize a new object.
@@ -106,10 +114,14 @@ module Dynamoid #:nodoc:
106
114
  # @since 0.2.0
107
115
  def exists?(id_or_conditions = {})
108
116
  case id_or_conditions
109
- when Hash then ! where(id_or_conditions).all.empty?
110
- else !! find(id_or_conditions)
117
+ when Hash then where(id_or_conditions).first.present?
118
+ else !! find_by_id(id_or_conditions)
111
119
  end
112
120
  end
121
+
122
+ def deep_subclasses
123
+ subclasses + subclasses.map(&:deep_subclasses).flatten
124
+ end
113
125
  end
114
126
 
115
127
  # Initialize a new object.
@@ -167,7 +179,7 @@ module Dynamoid #:nodoc:
167
179
  # @since 0.2.0
168
180
  def reload
169
181
  range_key_value = range_value ? dumped_range_value : nil
170
- self.attributes = self.class.find(hash_key, :range_key => range_key_value, :consistent_read => true).attributes
182
+ self.attributes = self.class.find(hash_key, range_key: range_key_value, consistent_read: true).attributes
171
183
  @associations.values.each(&:reset)
172
184
  self
173
185
  end
@@ -27,7 +27,7 @@ module Dynamoid
27
27
  attr_reader :record
28
28
 
29
29
  def initialize(record)
30
- super("Failed to destroy item")
30
+ super('Failed to destroy item')
31
31
  @record = record
32
32
  end
33
33
  end
@@ -61,6 +61,9 @@ module Dynamoid
61
61
  end
62
62
  end
63
63
 
64
+ class RecordNotFound < Error
65
+ end
66
+
64
67
  class DocumentNotValid < Error
65
68
  attr_reader :document
66
69
 
@@ -23,7 +23,7 @@ module Dynamoid #:nodoc:
23
23
  field :created_at, :datetime
24
24
  field :updated_at, :datetime
25
25
 
26
- field :id #Default primary key
26
+ field :id # Default primary key
27
27
  end
28
28
 
29
29
  module ClassMethods
@@ -50,7 +50,7 @@ module Dynamoid #:nodoc:
50
50
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
51
51
  type = :number
52
52
  end
53
- self.attributes = attributes.merge(name => {:type => type}.merge(options))
53
+ self.attributes = attributes.merge(name => {type: type}.merge(options))
54
54
 
55
55
  generated_methods.module_eval do
56
56
  define_method(named) { read_attribute(named) }
@@ -67,13 +67,13 @@ module Dynamoid #:nodoc:
67
67
  end
68
68
  end
69
69
 
70
- def range(name, type = :string)
71
- field(name, type)
70
+ def range(name, type = :string, options = {})
71
+ field(name, type, options)
72
72
  self.range_key = name
73
73
  end
74
74
 
75
75
  def table(options)
76
- #a default 'id' column is created when Dynamoid::Document is included
76
+ # a default 'id' column is created when Dynamoid::Document is included
77
77
  unless(attributes.has_key? hash_key)
78
78
  remove_field :id
79
79
  field(hash_key)
@@ -82,7 +82,7 @@ module Dynamoid #:nodoc:
82
82
 
83
83
  def remove_field(field)
84
84
  field = field.to_sym
85
- attributes.delete(field) or raise "No such field"
85
+ attributes.delete(field) or raise 'No such field'
86
86
 
87
87
  generated_methods.module_eval do
88
88
  remove_method field
@@ -36,9 +36,19 @@ module Dynamoid
36
36
  ids = Array(ids.flatten.uniq)
37
37
  if ids.count == 1
38
38
  result = self.find_by_id(ids.first, options)
39
+ if result.nil?
40
+ message = "Couldn't find #{self.name} with '#{self.hash_key}'=#{ids[0]}"
41
+ raise Errors::RecordNotFound.new(message)
42
+ end
39
43
  expects_array ? Array(result) : result
40
44
  else
41
- find_all(ids)
45
+ result = find_all(ids)
46
+ if result.size != ids.size
47
+ message = "Couldn't find all #{self.name.pluralize} with '#{self.hash_key}': (#{ids.join(', ')}) "
48
+ message << "(found #{result.size} results, but was looking for #{ids.size})"
49
+ raise Errors::RecordNotFound.new(message)
50
+ end
51
+ result
42
52
  end
43
53
  end
44
54
 
@@ -80,7 +90,7 @@ module Dynamoid
80
90
  # @param [String/Number] range_key of the object to find
81
91
  #
82
92
  def find_by_composite_key(hash_key, range_key, options = {})
83
- find_by_id(hash_key, options.merge({:range_key => range_key}))
93
+ find_by_id(hash_key, options.merge(range_key: range_key))
84
94
  end
85
95
 
86
96
  # Find all objects by hash and range keys.
@@ -105,7 +115,7 @@ module Dynamoid
105
115
  # @return [Array] an array of all matching items
106
116
  #
107
117
  def find_all_by_composite_key(hash_key, options = {})
108
- Dynamoid.adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
118
+ Dynamoid.adapter.query(self.table_name, options.merge(hash_value: hash_key)).collect do |item|
109
119
  from_database(item)
110
120
  end
111
121
  end
@@ -138,9 +148,9 @@ module Dynamoid
138
148
 
139
149
  if range_key_field
140
150
  range_key_field = range_key_field.to_s
141
- range_key_op = "eq"
142
- if range_key_field.include?(".")
143
- range_key_field, range_key_op = range_key_field.split(".", 2)
151
+ range_key_op = 'eq'
152
+ if range_key_field.include?('.')
153
+ range_key_field, range_key_op = range_key_field.split('.', 2)
144
154
  end
145
155
  range_op_mapped = RANGE_MAP.fetch(range_key_op)
146
156
  end
@@ -151,9 +161,9 @@ module Dynamoid
151
161
 
152
162
  # query
153
163
  opts = {
154
- :hash_key => hash_key_field.to_s,
155
- :hash_value => hash_key_value,
156
- :index_name => index.name,
164
+ hash_key: hash_key_field.to_s,
165
+ hash_value: hash_key_value,
166
+ index_name: index.name,
157
167
  }
158
168
  if range_key_field
159
169
  opts[:range_key] = range_key_field
@@ -80,7 +80,6 @@ module Dynamoid
80
80
  super
81
81
  end
82
82
 
83
-
84
83
  def identity_map_key
85
84
  key = hash_key.to_s
86
85
  if self.class.range_key
@@ -39,8 +39,8 @@ module Dynamoid
39
39
  end
40
40
 
41
41
  index_opts = {
42
- :read_capacity => Dynamoid::Config.read_capacity,
43
- :write_capacity => Dynamoid::Config.write_capacity
42
+ read_capacity: Dynamoid::Config.read_capacity,
43
+ write_capacity: Dynamoid::Config.write_capacity
44
44
  }.merge(options)
45
45
 
46
46
  index_opts[:dynamoid_class] = self
@@ -52,7 +52,6 @@ module Dynamoid
52
52
  self
53
53
  end
54
54
 
55
-
56
55
  # Defines a local secondary index on a table. Will use the same primary
57
56
  # hash key as the table.
58
57
  #
@@ -83,11 +82,10 @@ module Dynamoid
83
82
  ' must use a different :range_key than the primary key')
84
83
  end
85
84
 
86
- index_opts = options.merge({
87
- :dynamoid_class => self,
88
- :type => :local_secondary,
89
- :hash_key => primary_hash_key
90
- })
85
+ index_opts = options.merge(
86
+ dynamoid_class: self,
87
+ type: :local_secondary,
88
+ hash_key: primary_hash_key)
91
89
 
92
90
  index = Dynamoid::Indexes::Index.new(index_opts)
93
91
  key = index_key(primary_hash_key, index_range_key)
@@ -95,13 +93,11 @@ module Dynamoid
95
93
  self
96
94
  end
97
95
 
98
-
99
96
  def find_index(hash, range=nil)
100
97
  index = self.indexes[index_key(hash, range)]
101
98
  index
102
99
  end
103
100
 
104
-
105
101
  # Returns true iff the provided hash[,range] key combo is a local
106
102
  # secondary index.
107
103
  #
@@ -113,7 +109,6 @@ module Dynamoid
113
109
  self.local_secondary_indexes[index_key(hash, range)].present?
114
110
  end
115
111
 
116
-
117
112
  # Returns true iff the provided hash[,range] key combo is a global
118
113
  # secondary index.
119
114
  #
@@ -125,7 +120,6 @@ module Dynamoid
125
120
  self.global_secondary_indexes[index_key(hash, range)].present?
126
121
  end
127
122
 
128
-
129
123
  # Generates a convenient lookup key name for a hash/range index.
130
124
  # Should normally not be used directly.
131
125
  #
@@ -140,7 +134,6 @@ module Dynamoid
140
134
  name
141
135
  end
142
136
 
143
-
144
137
  # Generates a default index name.
145
138
  #
146
139
  # @param [Symbol] hash hash key name.
@@ -150,7 +143,6 @@ module Dynamoid
150
143
  "#{self.table_name}_index_#{self.index_key(hash, range)}"
151
144
  end
152
145
 
153
-
154
146
  # Convenience method to return all indexes on the table.
155
147
  #
156
148
  # @return [Hash<String, Object>] the combined hash of global and local
@@ -166,7 +158,6 @@ module Dynamoid
166
158
  end
167
159
  end
168
160
 
169
-
170
161
  # Represents the attributes of a DynamoDB index.
171
162
  class Index
172
163
  include ActiveModel::Validations
@@ -178,7 +169,6 @@ module Dynamoid
178
169
  :hash_key_schema, :range_key_schema, :projected_attributes,
179
170
  :read_capacity, :write_capacity
180
171
 
181
-
182
172
  validate do
183
173
  validate_index_type
184
174
  validate_hash_key
@@ -186,7 +176,6 @@ module Dynamoid
186
176
  validate_projected_attributes
187
177
  end
188
178
 
189
-
190
179
  def initialize(attrs={})
191
180
  unless attrs[:dynamoid_class].present?
192
181
  raise Dynamoid::Errors::InvalidIndex.new(':dynamoid_class is required')
@@ -205,7 +194,6 @@ module Dynamoid
205
194
  raise Dynamoid::Errors::InvalidIndex.new(self) unless self.valid?
206
195
  end
207
196
 
208
-
209
197
  # Convenience method to determine the projection type for an index.
210
198
  # Projection types are: :keys_only, :all, :include.
211
199
  #
@@ -218,56 +206,55 @@ module Dynamoid
218
206
  end
219
207
  end
220
208
 
221
-
222
209
  private
223
210
 
224
- def validate_projected_attributes
225
- unless (@projected_attributes.is_a?(Array) ||
226
- PROJECTION_TYPES.include?(@projected_attributes))
227
- errors.add(:projected_attributes, 'Invalid projected attributes specified.')
228
- end
211
+ def validate_projected_attributes
212
+ unless (@projected_attributes.is_a?(Array) ||
213
+ PROJECTION_TYPES.include?(@projected_attributes))
214
+ errors.add(:projected_attributes, 'Invalid projected attributes specified.')
229
215
  end
216
+ end
230
217
 
231
- def validate_index_type
232
- unless (@type.present? &&
233
- [:local_secondary, :global_secondary].include?(@type))
234
- errors.add(:type, 'Invalid index :type specified')
235
- end
218
+ def validate_index_type
219
+ unless (@type.present? &&
220
+ [:local_secondary, :global_secondary].include?(@type))
221
+ errors.add(:type, 'Invalid index :type specified')
236
222
  end
223
+ end
237
224
 
238
- def validate_range_key
239
- if @range_key.present?
240
- range_field_attributes = @dynamoid_class.attributes[@range_key]
241
- if range_field_attributes.present?
242
- range_key_type = range_field_attributes[:type]
243
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
244
- @range_key_schema = {
245
- @range_key => @dynamoid_class.dynamo_type(range_key_type)
246
- }
247
- else
248
- errors.add(:range_key, 'Index :range_key is not a valid key type')
249
- end
225
+ def validate_range_key
226
+ if @range_key.present?
227
+ range_field_attributes = @dynamoid_class.attributes[@range_key]
228
+ if range_field_attributes.present?
229
+ range_key_type = range_field_attributes[:type]
230
+ if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
231
+ @range_key_schema = {
232
+ @range_key => @dynamoid_class.dynamo_type(range_key_type)
233
+ }
250
234
  else
251
- errors.add(:range_key, "No such field #{@range_key} defined on table")
235
+ errors.add(:range_key, 'Index :range_key is not a valid key type')
252
236
  end
237
+ else
238
+ errors.add(:range_key, "No such field #{@range_key} defined on table")
253
239
  end
254
240
  end
241
+ end
255
242
 
256
- def validate_hash_key
257
- hash_field_attributes = @dynamoid_class.attributes[@hash_key]
258
- if hash_field_attributes.present?
259
- hash_field_type = hash_field_attributes[:type]
260
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
261
- @hash_key_schema = {
262
- @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
263
- }
264
- else
265
- errors.add(:hash_key, 'Index :hash_key is not a valid key type')
266
- end
243
+ def validate_hash_key
244
+ hash_field_attributes = @dynamoid_class.attributes[@hash_key]
245
+ if hash_field_attributes.present?
246
+ hash_field_type = hash_field_attributes[:type]
247
+ if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
248
+ @hash_key_schema = {
249
+ @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
250
+ }
267
251
  else
268
- errors.add(:hash_key, "No such field #{@hash_key} defined on table")
252
+ errors.add(:hash_key, 'Index :hash_key is not a valid key type')
269
253
  end
254
+ else
255
+ errors.add(:hash_key, "No such field #{@hash_key} defined on table")
270
256
  end
257
+ end
271
258
  end
272
259
  end
273
260
  end