dynamoid 3.10.0 → 3.12.1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  3. data/README.md +268 -8
  4. data/dynamoid.gemspec +4 -4
  5. data/lib/dynamoid/adapter.rb +1 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
  13. data/lib/dynamoid/components.rb +1 -0
  14. data/lib/dynamoid/config.rb +3 -0
  15. data/lib/dynamoid/criteria/chain.rb +74 -21
  16. data/lib/dynamoid/criteria/where_conditions.rb +13 -6
  17. data/lib/dynamoid/dirty.rb +97 -11
  18. data/lib/dynamoid/dumping.rb +39 -17
  19. data/lib/dynamoid/errors.rb +30 -3
  20. data/lib/dynamoid/fields.rb +13 -3
  21. data/lib/dynamoid/finders.rb +44 -23
  22. data/lib/dynamoid/loadable.rb +1 -0
  23. data/lib/dynamoid/persistence/inc.rb +35 -19
  24. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  25. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  26. data/lib/dynamoid/persistence/save.rb +29 -14
  27. data/lib/dynamoid/persistence/update_fields.rb +23 -8
  28. data/lib/dynamoid/persistence/update_validations.rb +3 -3
  29. data/lib/dynamoid/persistence/upsert.rb +22 -8
  30. data/lib/dynamoid/persistence.rb +184 -28
  31. data/lib/dynamoid/transaction_read/find.rb +137 -0
  32. data/lib/dynamoid/transaction_read.rb +146 -0
  33. data/lib/dynamoid/transaction_write/base.rb +47 -0
  34. data/lib/dynamoid/transaction_write/create.rb +49 -0
  35. data/lib/dynamoid/transaction_write/delete_with_instance.rb +65 -0
  36. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
  37. data/lib/dynamoid/transaction_write/destroy.rb +84 -0
  38. data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
  39. data/lib/dynamoid/transaction_write/save.rb +169 -0
  40. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  41. data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
  42. data/lib/dynamoid/transaction_write/upsert.rb +106 -0
  43. data/lib/dynamoid/transaction_write.rb +673 -0
  44. data/lib/dynamoid/type_casting.rb +3 -1
  45. data/lib/dynamoid/undumping.rb +13 -2
  46. data/lib/dynamoid/validations.rb +8 -5
  47. data/lib/dynamoid/version.rb +1 -1
  48. data/lib/dynamoid.rb +8 -0
  49. metadata +21 -5
@@ -14,12 +14,12 @@ module Dynamoid
14
14
  # specified +raise_error: false+ option then +find+ will not raise the
15
15
  # exception.
16
16
  #
17
- # When a document schema includes range key it always should be specified
17
+ # When a document schema includes range key it should always be specified
18
18
  # in +find+ method call. In case it's missing +MissingRangeKey+ exception
19
19
  # will be raised.
20
20
  #
21
21
  # Please note that +find+ doesn't preserve order of models in result when
22
- # passes multiple ids.
22
+ # given multiple ids.
23
23
  #
24
24
  # Supported following options:
25
25
  # * +consistent_read+
@@ -78,7 +78,7 @@ module Dynamoid
78
78
  # # Find all the tweets using hash key and range key with consistent read
79
79
  # Tweet.find_all([['1', 'red'], ['1', 'green']], consistent_read: true)
80
80
  def find_all(ids, options = {})
81
- 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')
82
82
 
83
83
  _find_all(ids, options)
84
84
  end
@@ -100,21 +100,35 @@ module Dynamoid
100
100
  #
101
101
  # @since 0.2.0
102
102
  def find_by_id(id, options = {})
103
- 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')
104
104
 
105
105
  _find_by_id(id, options)
106
106
  end
107
107
 
108
108
  # @private
109
109
  def _find_all(ids, options = {})
110
- raise Errors::MissingRangeKey if range_key && ids.any? { |_pk, sk| sk.nil? }
111
-
112
- if range_key
113
- ids = ids.map do |pk, sk|
114
- sk_casted = TypeCasting.cast_field(sk, attributes[range_key])
115
- sk_dumped = Dumping.dump_field(sk_casted, attributes[range_key])
110
+ ids = ids.map do |id|
111
+ if range_key
112
+ # expect [hash key, range key] pair
113
+ pk, sk = id
114
+
115
+ if pk.nil?
116
+ raise Errors::MissingHashKey
117
+ end
118
+ if sk.nil?
119
+ raise Errors::MissingRangeKey
120
+ end
121
+
122
+ pk_dumped = cast_and_dump(hash_key, pk)
123
+ sk_dumped = cast_and_dump(range_key, sk)
124
+
125
+ [pk_dumped, sk_dumped]
126
+ else
127
+ if id.nil?
128
+ raise Errors::MissingHashKey
129
+ end
116
130
 
117
- [pk, sk_dumped]
131
+ cast_and_dump(hash_key, id)
118
132
  end
119
133
  end
120
134
 
@@ -144,7 +158,7 @@ module Dynamoid
144
158
  models.each { |m| m.run_callbacks :find }
145
159
  models
146
160
  else
147
- ids_list = range_key ? ids.map { |pk, sk| "(#{pk},#{sk})" } : ids.map(&:to_s)
161
+ ids_list = range_key ? ids.map { |pk, sk| "(#{pk.inspect},#{sk.inspect})" } : ids.map(&:inspect)
148
162
  message = "Couldn't find all #{name.pluralize} with primary keys [#{ids_list.join(', ')}] "
149
163
  message += "(found #{items.size} results, but was looking for #{ids.size})"
150
164
  raise Errors::RecordNotFound, message
@@ -153,22 +167,21 @@ module Dynamoid
153
167
 
154
168
  # @private
155
169
  def _find_by_id(id, options = {})
170
+ raise Errors::MissingHashKey if id.nil?
156
171
  raise Errors::MissingRangeKey if range_key && options[:range_key].nil?
157
172
 
158
- if range_key
159
- key = options[:range_key]
160
- key_casted = TypeCasting.cast_field(key, attributes[range_key])
161
- key_dumped = Dumping.dump_field(key_casted, attributes[range_key])
173
+ partition_key_dumped = cast_and_dump(hash_key, id)
162
174
 
163
- options[:range_key] = key_dumped
175
+ if range_key
176
+ options[:range_key] = cast_and_dump(range_key, options[:range_key])
164
177
  end
165
178
 
166
- if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
179
+ if item = Dynamoid.adapter.read(table_name, partition_key_dumped, options.slice(:range_key, :consistent_read))
167
180
  model = from_database(item)
168
181
  model.run_callbacks :find
169
182
  model
170
183
  elsif options[:raise_error]
171
- primary_key = range_key ? "(#{id},#{options[:range_key]})" : id
184
+ primary_key = range_key ? "(#{id.inspect},#{options[:range_key].inspect})" : id.inspect
172
185
  message = "Couldn't find #{name} with primary key #{primary_key}"
173
186
  raise Errors::RecordNotFound, message
174
187
  end
@@ -180,7 +193,7 @@ module Dynamoid
180
193
  # @param range_key [Scalar value] range key of the object to find
181
194
  #
182
195
  def find_by_composite_key(hash_key, range_key, options = {})
183
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
196
+ Dynamoid.deprecator.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
184
197
 
185
198
  _find_by_id(hash_key, options.merge(range_key: range_key))
186
199
  end
@@ -207,7 +220,7 @@ module Dynamoid
207
220
  #
208
221
  # @return [Array] an array of all matching items
209
222
  def find_all_by_composite_key(hash_key, options = {})
210
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
223
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
211
224
 
212
225
  Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).flat_map { |i| i }.collect do |item|
213
226
  from_database(item)
@@ -237,7 +250,7 @@ module Dynamoid
237
250
  # @param options [Hash] conditions on range key e.g. +{ "rank.lte": 10 }, query filter, projected keys, scan_index_forward etc.
238
251
  # @return [Array] an array of all matching items
239
252
  def find_all_by_secondary_index(hash, options = {})
240
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
253
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
241
254
 
242
255
  range = options[:range] || {}
243
256
  hash_key_field, hash_key_value = hash.first
@@ -291,7 +304,7 @@ module Dynamoid
291
304
  def method_missing(method, *args)
292
305
  # Cannot use Symbol#start_with? because it was introduced in Ruby 2.7, but we support Ruby >= 2.3
293
306
  if method.to_s.start_with?('find')
294
- ActiveSupport::Deprecation.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
307
+ Dynamoid.deprecator.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
295
308
 
296
309
  finder = method.to_s.split('_by_').first
297
310
  attributes = method.to_s.split('_by_').last.split('_and_')
@@ -308,6 +321,14 @@ module Dynamoid
308
321
  super
309
322
  end
310
323
  end
324
+
325
+ private
326
+
327
+ def cast_and_dump(name, value)
328
+ attribute_options = attributes[name]
329
+ casted_value = TypeCasting.cast_field(value, attribute_options)
330
+ Dumping.dump_field(casted_value, attribute_options)
331
+ end
311
332
  end
312
333
  end
313
334
  end
@@ -11,6 +11,7 @@ module Dynamoid
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.
@@ -1,56 +1,66 @@
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
6
8
  class Inc
7
- def self.call(model_class, hash_key, range_key = nil, counters)
8
- new(model_class, hash_key, range_key, counters).call
9
+ def self.call(model_class, partition_key, sort_key = nil, counters)
10
+ new(model_class, partition_key, sort_key, counters).call
9
11
  end
10
12
 
11
13
  # rubocop:disable Style/OptionalArguments
12
- def initialize(model_class, hash_key, range_key = nil, counters)
14
+ def initialize(model_class, partition_key, sort_key = nil, counters)
13
15
  @model_class = model_class
14
- @hash_key = hash_key
15
- @range_key = range_key
16
+ @partition_key = partition_key
17
+ @sort_key = sort_key
16
18
  @counters = counters
17
19
  end
18
20
  # rubocop:enable Style/OptionalArguments
19
21
 
20
22
  def call
21
23
  touch = @counters.delete(:touch)
24
+ partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
25
+ options = update_item_options(partition_key_dumped)
26
+
27
+ Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options) do |t|
28
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
22
29
 
23
- Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
24
30
  @counters.each do |name, value|
25
- t.add(name => cast_and_dump_attribute_value(name, value))
31
+ item_updater.add(name => value)
26
32
  end
27
33
 
28
34
  if touch
29
35
  value = DateTime.now.in_time_zone(Time.zone)
30
36
 
31
37
  timestamp_attributes_to_touch(touch).each do |name|
32
- t.set(name => cast_and_dump_attribute_value(name, value))
38
+ item_updater.set(name => value)
33
39
  end
34
40
  end
35
41
  end
42
+ rescue Dynamoid::Errors::ConditionalCheckFailedException # rubocop:disable Lint/SuppressedException
36
43
  end
37
44
 
38
45
  private
39
46
 
40
- def update_item_options
47
+ def update_item_options(partition_key_dumped)
48
+ options = {}
49
+
50
+ conditions = {
51
+ if: {
52
+ @model_class.hash_key => partition_key_dumped
53
+ }
54
+ }
55
+ options[:conditions] = conditions
56
+
41
57
  if @model_class.range_key
42
- range_key_options = @model_class.attributes[@model_class.range_key]
43
- value_casted = TypeCasting.cast_field(@range_key, range_key_options)
44
- value_dumped = Dumping.dump_field(value_casted, range_key_options)
45
- { range_key: value_dumped }
46
- else
47
- {}
58
+ sort_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
59
+ options[:range_key] = sort_key_dumped
60
+ options[:conditions][@model_class.range_key] = sort_key_dumped
48
61
  end
49
- end
50
62
 
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])
63
+ options
54
64
  end
55
65
 
56
66
  def timestamp_attributes_to_touch(touch)
@@ -61,6 +71,12 @@ module Dynamoid
61
71
  names += Array.wrap(touch) if touch != true
62
72
  names
63
73
  end
74
+
75
+ def cast_and_dump(name, value)
76
+ options = @model_class.attributes[name]
77
+ value_casted = TypeCasting.cast_field(value, options)
78
+ Dumping.dump_field(value_casted, options)
79
+ end
64
80
  end
65
81
  end
66
82
  end
@@ -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
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Persistence
5
+ # @private
6
+ class ItemUpdaterWithDumping
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(dump(attributes))
14
+ end
15
+
16
+ def set(attributes)
17
+ @item_updater.set(dump(attributes))
18
+ end
19
+
20
+ private
21
+
22
+ def dump(attributes)
23
+ dumped = {}
24
+
25
+ attributes.each do |name, value|
26
+ dumped[name] = Dumping.dump_field(value, @model_class.attributes[name])
27
+ end
28
+
29
+ dumped
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item_updater_with_dumping'
4
+
3
5
  module Dynamoid
4
6
  module Persistence
5
7
  # @private
@@ -10,10 +12,12 @@ module Dynamoid
10
12
 
11
13
  def initialize(model, touch: nil)
12
14
  @model = model
13
- @touch = touch # touch=false means explicit disabling of updating the `updated_at` attribute
15
+ @touch = touch # `touch: false` means explicit disabling of updating the `updated_at` attribute
14
16
  end
15
17
 
16
18
  def call
19
+ validate_primary_key!
20
+
17
21
  @model.hash_key = SecureRandom.uuid if @model.hash_key.blank?
18
22
 
19
23
  return true unless @model.changed?
@@ -34,11 +38,14 @@ module Dynamoid
34
38
  Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
35
39
  else
36
40
  attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
41
+ partition_key_dumped = dump(@model.class.hash_key, @model.hash_key)
42
+ options = options_to_update_item(partition_key_dumped)
43
+
44
+ Dynamoid.adapter.update_item(@model.class.table_name, partition_key_dumped, options) do |t|
45
+ item_updater = ItemUpdaterWithDumping.new(@model.class, t)
37
46
 
38
- Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
39
47
  attributes_to_persist.each do |name, value|
40
- value_dumped = Dumping.dump_field(value, @model.class.attributes[name])
41
- t.set(name => value_dumped)
48
+ item_updater.set(name => value)
42
49
  end
43
50
  end
44
51
  end
@@ -55,22 +62,25 @@ module Dynamoid
55
62
 
56
63
  private
57
64
 
65
+ def validate_primary_key!
66
+ raise Dynamoid::Errors::MissingHashKey if !@model.new_record? && @model.hash_key.nil?
67
+ raise Dynamoid::Errors::MissingRangeKey if @model.class.range_key? && @model.range_value.nil?
68
+ end
69
+
58
70
  # Should be called after incrementing `lock_version` attribute
59
71
  def conditions_for_write
60
72
  conditions = {}
61
73
 
62
74
  # Add an 'exists' check to prevent overwriting existing records with new ones
63
- if @model.new_record?
64
- conditions[:unless_exists] = [@model.class.hash_key]
65
- if @model.range_key
66
- conditions[:unless_exists] << @model.range_key
67
- end
75
+ conditions[:unless_exists] = [@model.class.hash_key]
76
+ if @model.range_key
77
+ conditions[:unless_exists] << @model.range_key
68
78
  end
69
79
 
70
80
  # Add an optimistic locking check if the lock_version column exists
71
81
  # Uses the original lock_version value from Dirty API
72
82
  # in case user changed 'lock_version' manually
73
- if @model.class.attributes[:lock_version] && (@model.changes[:lock_version][0])
83
+ if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
74
84
  conditions[:if] ||= {}
75
85
  conditions[:if][:lock_version] = @model.changes[:lock_version][0]
76
86
  end
@@ -78,22 +88,22 @@ module Dynamoid
78
88
  conditions
79
89
  end
80
90
 
81
- def options_to_update_item
91
+ def options_to_update_item(partition_key_dumped)
82
92
  options = {}
83
93
 
84
94
  if @model.class.range_key
85
- value_dumped = Dumping.dump_field(@model.range_value, @model.class.attributes[@model.class.range_key])
95
+ value_dumped = dump(@model.class.range_key, @model.range_value)
86
96
  options[:range_key] = value_dumped
87
97
  end
88
98
 
89
99
  conditions = {}
90
100
  conditions[:if] ||= {}
91
- conditions[:if][@model.class.hash_key] = @model.hash_key
101
+ conditions[:if][@model.class.hash_key] = partition_key_dumped
92
102
 
93
103
  # Add an optimistic locking check if the lock_version column exists
94
104
  # Uses the original lock_version value from Dirty API
95
105
  # in case user changed 'lock_version' manually
96
- if @model.class.attributes[:lock_version] && (@model.changes[:lock_version][0])
106
+ if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
97
107
  conditions[:if] ||= {}
98
108
  conditions[:if][:lock_version] = @model.changes[:lock_version][0]
99
109
  end
@@ -102,6 +112,11 @@ module Dynamoid
102
112
 
103
113
  options
104
114
  end
115
+
116
+ def dump(name, value)
117
+ options = @model.class.attributes[name]
118
+ Dumping.dump_field(value, options)
119
+ end
105
120
  end
106
121
  end
107
122
  end
@@ -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
@@ -14,9 +16,12 @@ module Dynamoid
14
16
  @sort_key = sort_key
15
17
  @attributes = attributes.symbolize_keys
16
18
  @conditions = conditions
19
+
20
+ @partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
17
21
  end
18
22
 
19
23
  def call
24
+ validate_primary_key!
20
25
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
26
 
22
27
  if @model_class.timestamps_enabled?
@@ -30,12 +35,17 @@ module Dynamoid
30
35
 
31
36
  private
32
37
 
38
+ def validate_primary_key!
39
+ raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
40
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
41
+ end
42
+
33
43
  def update_item
34
- Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
44
+ Dynamoid.adapter.update_item(@model_class.table_name, @partition_key_dumped, options_to_update_item) do |t|
45
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
46
+
35
47
  @attributes.each do |k, v|
36
- value_casted = TypeCasting.cast_field(v, @model_class.attributes[k])
37
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[k])
38
- t.set(k => value_dumped)
48
+ item_updater.set(k => v)
39
49
  end
40
50
  end
41
51
  end
@@ -48,18 +58,23 @@ module Dynamoid
48
58
  options = {}
49
59
 
50
60
  if @model_class.range_key
51
- value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
52
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
53
- options[:range_key] = value_dumped
61
+ range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
62
+ options[:range_key] = range_key_dumped
54
63
  end
55
64
 
56
65
  conditions = @conditions.deep_dup
57
66
  conditions[:if] ||= {}
58
- conditions[:if][@model_class.hash_key] = @partition_key
67
+ conditions[:if][@model_class.hash_key] = @partition_key_dumped
59
68
  options[:conditions] = conditions
60
69
 
61
70
  options
62
71
  end
72
+
73
+ def cast_and_dump(name, value)
74
+ options = @model_class.attributes[name]
75
+ value_casted = TypeCasting.cast_field(value, options)
76
+ Dumping.dump_field(value_casted, options)
77
+ end
63
78
  end
64
79
  end
65
80
  end
@@ -7,9 +7,9 @@ module Dynamoid
7
7
  def self.validate_attributes_exist(model_class, attributes)
8
8
  model_attributes = model_class.attributes.keys
9
9
 
10
- attributes.each_key do |attr_name|
11
- unless model_attributes.include?(attr_name)
12
- raise Dynamoid::Errors::UnknownAttribute, "Attribute #{attr_name} does not exist in #{model_class}"
10
+ attributes.each_key do |name|
11
+ unless model_attributes.include?(name)
12
+ raise Dynamoid::Errors::UnknownAttribute.new(model_class, name)
13
13
  end
14
14
  end
15
15
  end
@@ -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
@@ -17,6 +19,7 @@ module Dynamoid
17
19
  end
18
20
 
19
21
  def call
22
+ validate_primary_key!
20
23
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
24
 
22
25
  if @model_class.timestamps_enabled?
@@ -30,13 +33,19 @@ module Dynamoid
30
33
 
31
34
  private
32
35
 
36
+ def validate_primary_key!
37
+ raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
38
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
39
+ end
40
+
33
41
  def update_item
34
- Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
35
- @attributes.each do |k, v|
36
- value_casted = TypeCasting.cast_field(v, @model_class.attributes[k])
37
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[k])
42
+ partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
38
43
 
39
- t.set(k => value_dumped)
44
+ Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options_to_update_item) do |t|
45
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
46
+
47
+ @attributes.each do |k, v|
48
+ item_updater.set(k => v)
40
49
  end
41
50
  end
42
51
  end
@@ -45,9 +54,8 @@ module Dynamoid
45
54
  options = {}
46
55
 
47
56
  if @model_class.range_key
48
- value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
49
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
50
- options[:range_key] = value_dumped
57
+ range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
58
+ options[:range_key] = range_key_dumped
51
59
  end
52
60
 
53
61
  options[:conditions] = @conditions
@@ -57,6 +65,12 @@ module Dynamoid
57
65
  def undump_attributes(raw_attributes)
58
66
  Undumping.undump_attributes(raw_attributes, @model_class.attributes)
59
67
  end
68
+
69
+ def cast_and_dump(name, value)
70
+ options = @model_class.attributes[name]
71
+ value_casted = TypeCasting.cast_field(value, options)
72
+ Dumping.dump_field(value_casted, options)
73
+ end
60
74
  end
61
75
  end
62
76
  end