dynamoid 3.9.0 → 3.11.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/README.md +202 -25
  4. data/dynamoid.gemspec +5 -6
  5. data/lib/dynamoid/adapter.rb +19 -13
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
  14. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  15. data/lib/dynamoid/associations.rb +1 -1
  16. data/lib/dynamoid/components.rb +1 -0
  17. data/lib/dynamoid/config/options.rb +12 -12
  18. data/lib/dynamoid/config.rb +3 -0
  19. data/lib/dynamoid/criteria/chain.rb +149 -142
  20. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  21. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  22. data/lib/dynamoid/criteria/where_conditions.rb +36 -0
  23. data/lib/dynamoid/dirty.rb +87 -12
  24. data/lib/dynamoid/document.rb +1 -1
  25. data/lib/dynamoid/dumping.rb +38 -16
  26. data/lib/dynamoid/errors.rb +14 -2
  27. data/lib/dynamoid/fields/declare.rb +6 -6
  28. data/lib/dynamoid/fields.rb +6 -8
  29. data/lib/dynamoid/finders.rb +23 -32
  30. data/lib/dynamoid/indexes.rb +6 -7
  31. data/lib/dynamoid/loadable.rb +3 -2
  32. data/lib/dynamoid/persistence/inc.rb +6 -7
  33. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  34. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  35. data/lib/dynamoid/persistence/save.rb +17 -18
  36. data/lib/dynamoid/persistence/update_fields.rb +7 -5
  37. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  38. data/lib/dynamoid/persistence/upsert.rb +5 -4
  39. data/lib/dynamoid/persistence.rb +77 -21
  40. data/lib/dynamoid/transaction_write/base.rb +47 -0
  41. data/lib/dynamoid/transaction_write/create.rb +49 -0
  42. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  43. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  44. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  45. data/lib/dynamoid/transaction_write/save.rb +164 -0
  46. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  47. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  48. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  49. data/lib/dynamoid/transaction_write.rb +464 -0
  50. data/lib/dynamoid/type_casting.rb +18 -15
  51. data/lib/dynamoid/undumping.rb +14 -3
  52. data/lib/dynamoid/validations.rb +1 -1
  53. data/lib/dynamoid/version.rb +1 -1
  54. data/lib/dynamoid.rb +7 -0
  55. metadata +30 -16
  56. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  57. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -36,8 +36,11 @@ module Dynamoid
36
36
  end
37
37
  end
38
38
 
39
- def from_database(*)
40
- super.tap(&:clear_changes_information)
39
+ def from_database(attributes_from_database)
40
+ super.tap do |model|
41
+ model.clear_changes_information
42
+ model.assign_attributes_from_database(DeepDupper.dup_attributes(model.attributes, model.class))
43
+ end
41
44
  end
42
45
  end
43
46
 
@@ -108,7 +111,7 @@ module Dynamoid
108
111
  #
109
112
  # @return [ActiveSupport::HashWithIndifferentAccess]
110
113
  def changes
111
- ActiveSupport::HashWithIndifferentAccess[changed.map { |name| [name, attribute_change(name)] }]
114
+ ActiveSupport::HashWithIndifferentAccess[changed_attributes.map { |name, old_value| [name, [old_value, read_attribute(name)]] }]
112
115
  end
113
116
 
114
117
  # Returns a hash of attributes that were changed before the model was saved.
@@ -132,19 +135,21 @@ module Dynamoid
132
135
  #
133
136
  # @return [ActiveSupport::HashWithIndifferentAccess]
134
137
  def changed_attributes
135
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
138
+ attributes_changed_by_setter.merge(attributes_changed_in_place)
136
139
  end
137
140
 
138
141
  # Clear all dirty data: current changes and previous changes.
139
142
  def clear_changes_information
140
143
  @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
141
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
144
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
145
+ @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
142
146
  end
143
147
 
144
148
  # Clears dirty data and moves +changes+ to +previous_changes+.
145
149
  def changes_applied
146
150
  @previously_changed = changes
147
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
151
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
152
+ @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
148
153
  end
149
154
 
150
155
  # Remove changes information for the provided attributes.
@@ -152,6 +157,9 @@ module Dynamoid
152
157
  # @param attributes [Array[String]] - a list of attributes to clear changes for
153
158
  def clear_attribute_changes(names)
154
159
  attributes_changed_by_setter.except!(*names)
160
+
161
+ slice = HashWithIndifferentAccess.new(@attributes).slice(*names)
162
+ attributes_from_database.merge!(DeepDupper.dup_attributes(slice, self.class))
155
163
  end
156
164
 
157
165
  # Handle <tt>*_changed?</tt> for +method_missing+.
@@ -220,12 +228,16 @@ module Dynamoid
220
228
  previous_changes[name] if attribute_previously_changed?(name)
221
229
  end
222
230
 
231
+ # @private
232
+ def assign_attributes_from_database(attributes_from_database)
233
+ @attributes_from_database = HashWithIndifferentAccess.new(attributes_from_database)
234
+ end
235
+
223
236
  private
224
237
 
225
238
  def changes_include?(name)
226
- attributes_changed_by_setter.include?(name)
239
+ attribute_changed_by_setter?(name) || attribute_changed_in_place?(name)
227
240
  end
228
- alias attribute_changed_by_setter? changes_include?
229
241
 
230
242
  # Handle <tt>*_change</tt> for +method_missing+.
231
243
  def attribute_change(name)
@@ -238,7 +250,7 @@ module Dynamoid
238
250
 
239
251
  begin
240
252
  value = read_attribute(name)
241
- value = value.duplicable? ? value.clone : value
253
+ value = value.clone if value.duplicable?
242
254
  rescue TypeError, NoMethodError
243
255
  end
244
256
 
@@ -259,13 +271,76 @@ module Dynamoid
259
271
  previous_changes.include?(name)
260
272
  end
261
273
 
262
- # This is necessary because `changed_attributes` might be overridden in
263
- # other implemntations (e.g. in `ActiveRecord`)
264
- alias attributes_changed_by_setter changed_attributes
274
+ def attributes_changed_by_setter
275
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
276
+ end
277
+
278
+ def attribute_changed_by_setter?(name)
279
+ attributes_changed_by_setter.include?(name)
280
+ end
281
+
282
+ def attributes_from_database
283
+ @attributes_from_database ||= ActiveSupport::HashWithIndifferentAccess.new
284
+ end
265
285
 
266
286
  # Force an attribute to have a particular "before" value
267
287
  def set_attribute_was(name, old_value)
268
288
  attributes_changed_by_setter[name] = old_value
269
289
  end
290
+
291
+ def attributes_changed_in_place
292
+ attributes_from_database.select do |name, _|
293
+ attribute_changed_in_place?(name)
294
+ end
295
+ end
296
+
297
+ def attribute_changed_in_place?(name)
298
+ return false if attribute_changed_by_setter?(name)
299
+
300
+ value_from_database = attributes_from_database[name]
301
+ return false if value_from_database.nil?
302
+
303
+ value = read_attribute(name)
304
+ value != value_from_database
305
+ end
306
+
307
+ module DeepDupper
308
+ def self.dup_attributes(attributes, klass)
309
+ attributes.map do |name, value|
310
+ type_options = klass.attributes[name.to_sym]
311
+ value_duplicate = dup_attribute(value, type_options)
312
+ [name, value_duplicate]
313
+ end.to_h
314
+ end
315
+
316
+ def self.dup_attribute(value, type_options)
317
+ type, of = type_options.values_at(:type, :of)
318
+
319
+ case value
320
+ when NilClass, TrueClass, FalseClass, Numeric, Symbol, IO
321
+ # till Ruby 2.4 these immutable objects could not be duplicated
322
+ # IO objects cannot be duplicated - is used for binary fields
323
+ value
324
+ when String
325
+ value.dup
326
+ when Array
327
+ if of.is_a? Class # custom type
328
+ value.map { |e| dup_attribute(e, type: of) }
329
+ else
330
+ value.deep_dup
331
+ end
332
+ when Set
333
+ Set.new(value.map { |e| dup_attribute(e, type: of) })
334
+ when Hash
335
+ value.deep_dup
336
+ else
337
+ if type.is_a? Class # custom type
338
+ Marshal.load(Marshal.dump(value)) # dup instance variables
339
+ else
340
+ value.dup # date, datetime
341
+ end
342
+ end
343
+ end
344
+ end
270
345
  end
271
346
  end
@@ -260,7 +260,7 @@ module Dynamoid
260
260
  #
261
261
  # @return [Integer]
262
262
  def hash
263
- hash_key.hash ^ range_value.hash
263
+ [hash_key, range_value].hash
264
264
  end
265
265
 
266
266
  # Return a model's hash key value.
@@ -70,7 +70,8 @@ module Dynamoid
70
70
  end
71
71
 
72
72
  def invalid_value?(value)
73
- (value.is_a?(Set) || value.is_a?(String)) && value.empty?
73
+ (value.is_a?(Set) && value.empty?) ||
74
+ (value.is_a?(String) && value.empty? && Config.store_empty_string_as_nil)
74
75
  end
75
76
  end
76
77
 
@@ -86,6 +87,12 @@ module Dynamoid
86
87
 
87
88
  # string -> string
88
89
  class StringDumper < Base
90
+ def process(string)
91
+ return nil if string.nil?
92
+ return nil if string.empty? && Config.store_empty_string_as_nil
93
+
94
+ string
95
+ end
89
96
  end
90
97
 
91
98
  # integer -> number
@@ -101,6 +108,8 @@ module Dynamoid
101
108
  ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
102
109
 
103
110
  def process(set)
111
+ return nil if set.is_a?(Set) && set.empty?
112
+
104
113
  if @options.key?(:of)
105
114
  process_typed_collection(set)
106
115
  else
@@ -112,13 +121,13 @@ module Dynamoid
112
121
 
113
122
  def process_typed_collection(set)
114
123
  if allowed_type?
115
- dumper = Dumping.find_dumper(element_options)
116
- result = set.map { |el| dumper.process(el) }
117
-
118
- if element_type == :string
119
- result.reject!(&:empty?)
124
+ # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
125
+ if element_type == :string && Config.store_empty_string_as_nil
126
+ set.reject! { |s| s && s.empty? }
120
127
  end
121
128
 
129
+ dumper = Dumping.find_dumper(element_options)
130
+ result = set.map { |el| dumper.process(el) }
122
131
  result.to_set
123
132
  else
124
133
  raise ArgumentError, "Set element type #{element_type} isn't supported"
@@ -164,14 +173,13 @@ module Dynamoid
164
173
 
165
174
  def process_typed_collection(array)
166
175
  if allowed_type?
167
- dumper = Dumping.find_dumper(element_options)
168
- result = array.map { |el| dumper.process(el) }
169
-
170
- if element_type == :string
171
- result.reject!(&:empty?)
176
+ # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
177
+ if element_type == :string && Config.store_empty_string_as_nil
178
+ array.reject! { |s| s && s.empty? }
172
179
  end
173
180
 
174
- result
181
+ dumper = Dumping.find_dumper(element_options)
182
+ array.map { |el| dumper.process(el) }
175
183
  else
176
184
  raise ArgumentError, "Array element type #{element_type} isn't supported"
177
185
  end
@@ -210,7 +218,7 @@ module Dynamoid
210
218
  # datetime -> integer/string
211
219
  class DateTimeDumper < Base
212
220
  def process(value)
213
- !value.nil? ? format_datetime(value, @options) : nil
221
+ value.nil? ? nil : format_datetime(value, @options)
214
222
  end
215
223
 
216
224
  private
@@ -237,7 +245,7 @@ module Dynamoid
237
245
  # date -> integer/string
238
246
  class DateDumper < Base
239
247
  def process(value)
240
- !value.nil? ? format_date(value, @options) : nil
248
+ value.nil? ? nil : format_date(value, @options)
241
249
  end
242
250
 
243
251
  private
@@ -289,10 +297,24 @@ module Dynamoid
289
297
  end
290
298
  end
291
299
 
292
- # string -> string
300
+ # string -> StringIO
293
301
  class BinaryDumper < Base
294
302
  def process(value)
295
- Base64.strict_encode64(value)
303
+ store_as_binary = if @options[:store_as_native_binary].nil?
304
+ Dynamoid.config.store_binary_as_native
305
+ else
306
+ @options[:store_as_native_binary]
307
+ end
308
+
309
+ if store_as_binary
310
+ if value.is_a?(StringIO) || value.is_a?(IO)
311
+ value
312
+ else
313
+ StringIO.new(value)
314
+ end
315
+ else
316
+ Base64.strict_encode64(value)
317
+ end
296
318
  end
297
319
  end
298
320
 
@@ -6,6 +6,7 @@ module Dynamoid
6
6
  # Generic Dynamoid error
7
7
  class Error < StandardError; end
8
8
 
9
+ class MissingHashKey < Error; end
9
10
  class MissingRangeKey < Error; end
10
11
 
11
12
  class MissingIndex < Error; end
@@ -15,18 +16,27 @@ module Dynamoid
15
16
  class InvalidIndex < Error
16
17
  def initialize(item)
17
18
  if item.is_a? String
18
- super(item)
19
+ super
19
20
  else
20
21
  super("Validation failed: #{item.errors.full_messages.join(', ')}")
21
22
  end
22
23
  end
23
24
  end
24
25
 
26
+ class RecordNotSaved < Error
27
+ attr_reader :record
28
+
29
+ def initialize(record)
30
+ super('Failed to save the item')
31
+ @record = record
32
+ end
33
+ end
34
+
25
35
  class RecordNotDestroyed < Error
26
36
  attr_reader :record
27
37
 
28
38
  def initialize(record)
29
- super('Failed to destroy item')
39
+ super('Failed to destroy the item')
30
40
  @record = record
31
41
  end
32
42
  end
@@ -79,5 +89,7 @@ module Dynamoid
79
89
  class UnknownAttribute < Error; end
80
90
 
81
91
  class SubclassNotFound < Error; end
92
+
93
+ class Rollback < Error; end
82
94
  end
83
95
  end
@@ -48,7 +48,7 @@ module Dynamoid
48
48
 
49
49
  @source.generated_methods.module_eval do
50
50
  define_method(name) { read_attribute(name) }
51
- define_method("#{name}?") do
51
+ define_method(:"#{name}?") do
52
52
  value = read_attribute(name)
53
53
  case value
54
54
  when true then true
@@ -57,8 +57,8 @@ module Dynamoid
57
57
  !value.nil?
58
58
  end
59
59
  end
60
- define_method("#{name}=") { |value| write_attribute(name, value) }
61
- define_method("#{name}_before_type_cast") { read_attribute_before_type_cast(name) }
60
+ define_method(:"#{name}=") { |value| write_attribute(name, value) }
61
+ define_method(:"#{name}_before_type_cast") { read_attribute_before_type_cast(name) }
62
62
  end
63
63
  end
64
64
 
@@ -70,9 +70,9 @@ module Dynamoid
70
70
 
71
71
  @source.generated_methods.module_eval do
72
72
  alias_method alias_name, name
73
- alias_method "#{alias_name}=", "#{name}="
74
- alias_method "#{alias_name}?", "#{name}?"
75
- alias_method "#{alias_name}_before_type_cast", "#{name}_before_type_cast"
73
+ alias_method :"#{alias_name}=", :"#{name}="
74
+ alias_method :"#{alias_name}?", :"#{name}?"
75
+ alias_method :"#{alias_name}_before_type_cast", :"#{name}_before_type_cast"
76
76
  end
77
77
  end
78
78
 
@@ -164,7 +164,7 @@ module Dynamoid
164
164
  #
165
165
  # @param name [Symbol] a range key attribute name
166
166
  # @param type [Symbol] a range key type (optional)
167
- # @param options [Symbol] type options (optional)
167
+ # @param options [Hash] type options (optional)
168
168
  def range(name, type = :string, options = {})
169
169
  field(name, type, options)
170
170
  self.range_key = name
@@ -263,10 +263,8 @@ module Dynamoid
263
263
 
264
264
  # @private
265
265
  def generated_methods
266
- @generated_methods ||= begin
267
- Module.new.tap do |mod|
268
- include(mod)
269
- end
266
+ @generated_methods ||= Module.new.tap do |mod|
267
+ include(mod)
270
268
  end
271
269
  end
272
270
  end
@@ -362,7 +360,7 @@ module Dynamoid
362
360
  seconds = options[:after]
363
361
 
364
362
  if self[name].blank?
365
- send("#{name}=", Time.now.to_i + seconds)
363
+ send(:"#{name}=", Time.now.to_i + seconds)
366
364
  end
367
365
  end
368
366
  end
@@ -374,12 +372,12 @@ module Dynamoid
374
372
 
375
373
  type = self.class.inheritance_field
376
374
  if self.class.attributes[type] && send(type).nil?
377
- send("#{type}=", self.class.sti_name)
375
+ send(:"#{type}=", self.class.sti_name)
378
376
  end
379
377
  end
380
378
 
381
379
  def attribute_is_present_on_model?(attribute_name)
382
- setter = "#{attribute_name}=".to_sym
380
+ setter = :"#{attribute_name}="
383
381
  respond_to?(setter)
384
382
  end
385
383
  end
@@ -6,17 +6,6 @@ module Dynamoid
6
6
  module Finders
7
7
  extend ActiveSupport::Concern
8
8
 
9
- # @private
10
- RANGE_MAP = {
11
- 'gt' => :range_greater_than,
12
- 'lt' => :range_less_than,
13
- 'gte' => :range_gte,
14
- 'lte' => :range_lte,
15
- 'begins_with' => :range_begins_with,
16
- 'between' => :range_between,
17
- 'eq' => :range_eq
18
- }.freeze
19
-
20
9
  module ClassMethods
21
10
  # Find one or many objects, specified by one id or an array of ids.
22
11
  #
@@ -89,7 +78,7 @@ module Dynamoid
89
78
  # # Find all the tweets using hash key and range key with consistent read
90
79
  # Tweet.find_all([['1', 'red'], ['1', 'green']], consistent_read: true)
91
80
  def find_all(ids, options = {})
92
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
81
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
93
82
 
94
83
  _find_all(ids, options)
95
84
  end
@@ -111,7 +100,7 @@ module Dynamoid
111
100
  #
112
101
  # @since 0.2.0
113
102
  def find_by_id(id, options = {})
114
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_id is deprecated! Call .find instead of', caller[1..-1])
103
+ Dynamoid.deprecator.warn('[Dynamoid] .find_by_id is deprecated! Call .find instead of')
115
104
 
116
105
  _find_by_id(id, options)
117
106
  end
@@ -191,7 +180,7 @@ module Dynamoid
191
180
  # @param range_key [Scalar value] range key of the object to find
192
181
  #
193
182
  def find_by_composite_key(hash_key, range_key, options = {})
194
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
183
+ Dynamoid.deprecator.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
195
184
 
196
185
  _find_by_id(hash_key, options.merge(range_key: range_key))
197
186
  end
@@ -218,7 +207,7 @@ module Dynamoid
218
207
  #
219
208
  # @return [Array] an array of all matching items
220
209
  def find_all_by_composite_key(hash_key, options = {})
221
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
210
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
222
211
 
223
212
  Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).flat_map { |i| i }.collect do |item|
224
213
  from_database(item)
@@ -248,12 +237,11 @@ module Dynamoid
248
237
  # @param options [Hash] conditions on range key e.g. +{ "rank.lte": 10 }, query filter, projected keys, scan_index_forward etc.
249
238
  # @return [Array] an array of all matching items
250
239
  def find_all_by_secondary_index(hash, options = {})
251
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
240
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
252
241
 
253
242
  range = options[:range] || {}
254
243
  hash_key_field, hash_key_value = hash.first
255
244
  range_key_field, range_key_value = range.first
256
- range_op_mapped = nil
257
245
 
258
246
  if range_key_field
259
247
  range_key_field = range_key_field.to_s
@@ -261,27 +249,30 @@ module Dynamoid
261
249
  if range_key_field.include?('.')
262
250
  range_key_field, range_key_op = range_key_field.split('.', 2)
263
251
  end
264
- range_op_mapped = RANGE_MAP.fetch(range_key_op)
265
252
  end
266
253
 
267
254
  # Find the index
268
255
  index = find_index(hash_key_field, range_key_field)
269
256
  raise Dynamoid::Errors::MissingIndex, "attempted to find #{[hash_key_field, range_key_field]}" if index.nil?
270
257
 
271
- # query
272
- opts = {
273
- hash_key: hash_key_field.to_s,
274
- hash_value: hash_key_value,
275
- index_name: index.name
276
- }
258
+ # Query
259
+ query_key_conditions = {}
260
+ query_key_conditions[hash_key_field.to_sym] = [[:eq, hash_key_value]]
277
261
  if range_key_field
278
- opts[:range_key] = range_key_field
279
- opts[range_op_mapped] = range_key_value
280
- end
281
- dynamo_options = opts.merge(options.reject { |key, _| key == :range })
282
- Dynamoid.adapter.query(table_name, dynamo_options).flat_map { |i| i }.map do |item|
283
- from_database(item)
262
+ query_key_conditions[range_key_field.to_sym] = [[range_key_op.to_sym, range_key_value]]
284
263
  end
264
+
265
+ query_non_key_conditions = options
266
+ .except(*Dynamoid::AdapterPlugin::AwsSdkV3::Query::OPTIONS_KEYS)
267
+ .except(:range)
268
+ .symbolize_keys
269
+
270
+ query_options = options.slice(*Dynamoid::AdapterPlugin::AwsSdkV3::Query::OPTIONS_KEYS)
271
+ query_options[:index_name] = index.name
272
+
273
+ Dynamoid.adapter.query(table_name, query_key_conditions, query_non_key_conditions, query_options)
274
+ .flat_map { |i| i }
275
+ .map { |item| from_database(item) }
285
276
  end
286
277
 
287
278
  # Find using exciting method_missing finders attributes. Uses criteria
@@ -300,7 +291,7 @@ module Dynamoid
300
291
  def method_missing(method, *args)
301
292
  # Cannot use Symbol#start_with? because it was introduced in Ruby 2.7, but we support Ruby >= 2.3
302
293
  if method.to_s.start_with?('find')
303
- ActiveSupport::Deprecation.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
294
+ Dynamoid.deprecator.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
304
295
 
305
296
  finder = method.to_s.split('_by_').first
306
297
  attributes = method.to_s.split('_by_').last.split('_and_')
@@ -308,7 +299,7 @@ module Dynamoid
308
299
  chain = Dynamoid::Criteria::Chain.new(self)
309
300
  chain = chain.where({}.tap { |h| attributes.each_with_index { |attr, index| h[attr.to_sym] = args[index] } })
310
301
 
311
- if finder =~ /all/
302
+ if finder.include?('all')
312
303
  chain.all
313
304
  else
314
305
  chain.first
@@ -130,13 +130,13 @@ module Dynamoid
130
130
  index_range_key = options[:range_key]
131
131
 
132
132
  unless index_range_key.present?
133
- raise Dynamoid::Errors::InvalidIndex, 'A local secondary index '\
134
- 'requires a :range_key to be specified'
133
+ raise Dynamoid::Errors::InvalidIndex, 'A local secondary index ' \
134
+ 'requires a :range_key to be specified'
135
135
  end
136
136
 
137
137
  if primary_range_key.present? && index_range_key == primary_range_key
138
- raise Dynamoid::Errors::InvalidIndex, 'A local secondary index'\
139
- ' must use a different :range_key than the primary key'
138
+ raise Dynamoid::Errors::InvalidIndex, 'A local secondary index ' \
139
+ 'must use a different :range_key than the primary key'
140
140
  end
141
141
 
142
142
  index_opts = options.merge(
@@ -159,8 +159,7 @@ module Dynamoid
159
159
  # @param range [scalar] the range key used to declare an index (optional)
160
160
  # @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
161
161
  def find_index(hash, range = nil)
162
- index = indexes[index_key(hash, range)]
163
- index
162
+ indexes[index_key(hash, range)]
164
163
  end
165
164
 
166
165
  # Returns an index by its name
@@ -317,7 +316,7 @@ module Dynamoid
317
316
 
318
317
  key_dynamodb_type = dynamodb_type(key_field_attributes[:type], key_field_attributes)
319
318
  if PERMITTED_KEY_DYNAMODB_TYPES.include?(key_dynamodb_type)
320
- send("#{key_param}_schema=", { key_val => key_dynamodb_type })
319
+ send(:"#{key_param}_schema=", { key_val => key_dynamodb_type })
321
320
  else
322
321
  errors.add(key_param, "Index :#{key_param} is not a valid key type")
323
322
  end
@@ -6,11 +6,12 @@ module Dynamoid
6
6
 
7
7
  def load(attrs)
8
8
  attrs.each do |key, value|
9
- send("#{key}=", value) if respond_to?("#{key}=")
9
+ send(:"#{key}=", value) if respond_to?(:"#{key}=")
10
10
  end
11
11
 
12
12
  self
13
13
  end
14
+ alias assign_attributes load
14
15
 
15
16
  # Reload an object from the database -- if you suspect the object has changed in the data store and you need those
16
17
  # changes to be reflected immediately, you would call this method. This is a consistent read.
@@ -27,7 +28,7 @@ module Dynamoid
27
28
 
28
29
  self.attributes = self.class.find(hash_key, **options).attributes
29
30
 
30
- @associations.values.each(&:reset)
31
+ @associations.each_value(&:reset)
31
32
  @new_record = false
32
33
 
33
34
  self
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item_updater_with_casting_and_dumping'
4
+
3
5
  module Dynamoid
4
6
  module Persistence
5
7
  # @private
@@ -21,15 +23,17 @@ module Dynamoid
21
23
  touch = @counters.delete(:touch)
22
24
 
23
25
  Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
26
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
27
+
24
28
  @counters.each do |name, value|
25
- t.add(name => cast_and_dump_attribute_value(name, value))
29
+ item_updater.add(name => value)
26
30
  end
27
31
 
28
32
  if touch
29
33
  value = DateTime.now.in_time_zone(Time.zone)
30
34
 
31
35
  timestamp_attributes_to_touch(touch).each do |name|
32
- t.set(name => cast_and_dump_attribute_value(name, value))
36
+ item_updater.set(name => value)
33
37
  end
34
38
  end
35
39
  end
@@ -48,11 +52,6 @@ module Dynamoid
48
52
  end
49
53
  end
50
54
 
51
- def cast_and_dump_attribute_value(name, value)
52
- value_casted = TypeCasting.cast_field(value, @model_class.attributes[name])
53
- Dumping.dump_field(value_casted, @model_class.attributes[name])
54
- end
55
-
56
55
  def timestamp_attributes_to_touch(touch)
57
56
  return [] unless touch
58
57
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Persistence
5
+ # @private
6
+ class ItemUpdaterWithCastingAndDumping
7
+ def initialize(model_class, item_updater)
8
+ @model_class = model_class
9
+ @item_updater = item_updater
10
+ end
11
+
12
+ def add(attributes)
13
+ @item_updater.add(cast_and_dump(attributes))
14
+ end
15
+
16
+ def set(attributes)
17
+ @item_updater.set(cast_and_dump(attributes))
18
+ end
19
+
20
+ private
21
+
22
+ def cast_and_dump(attributes)
23
+ casted_and_dumped = {}
24
+
25
+ attributes.each do |name, value|
26
+ value_casted = TypeCasting.cast_field(value, @model_class.attributes[name])
27
+ value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[name])
28
+
29
+ casted_and_dumped[name] = value_dumped
30
+ end
31
+
32
+ casted_and_dumped
33
+ end
34
+ end
35
+ end
36
+ end