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
@@ -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