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
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'dynamoid/persistence/update_validations'
5
+
6
+ module Dynamoid
7
+ class TransactionWrite
8
+ class UpdateFields < Base
9
+ def initialize(model_class, hash_key, range_key, attributes, &block)
10
+ super()
11
+
12
+ @model_class = model_class
13
+ @hash_key = hash_key
14
+ @range_key = range_key
15
+ @attributes = attributes || {}
16
+ @block = block
17
+ end
18
+
19
+ def on_registration
20
+ validate_primary_key!
21
+ Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
22
+
23
+ if @block
24
+ @item_updater = ItemUpdater.new(@model_class)
25
+ @block.call(@item_updater)
26
+ end
27
+ end
28
+
29
+ def on_commit; end
30
+
31
+ def on_rollback; end
32
+
33
+ def aborted?
34
+ false
35
+ end
36
+
37
+ def skipped?
38
+ @attributes.empty? && (!@item_updater || @item_updater.empty?)
39
+ end
40
+
41
+ def observable_by_user_result
42
+ nil
43
+ end
44
+
45
+ def action_request
46
+ builder = UpdateRequestBuilder.new(@model_class)
47
+
48
+ # primary key to look up an item to update
49
+ builder.hash_key = dump_attribute(@model_class.hash_key, @hash_key)
50
+ builder.range_key = dump_attribute(@model_class.range_key, @range_key) if @model_class.range_key?
51
+
52
+ # changed attributes to persist
53
+ changes = @attributes.dup
54
+ changes = add_timestamps(changes, skip_created_at: true)
55
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
56
+
57
+ builder.set_attributes(changes_dumped)
58
+
59
+ # given a block
60
+ if @item_updater
61
+ builder.set_attributes(@item_updater.attributes_to_set)
62
+ builder.remove_attributes(@item_updater.attributes_to_remove)
63
+
64
+ @item_updater.attributes_to_add.each do |name, value|
65
+ # The ADD section in UpdateExpressions requires values to be a
66
+ # set to update a set attribute.
67
+ # Allow specifying values as any Enumerable collection (e.g. Array).
68
+ # Allow a single value not wrapped into a Set
69
+ if @model_class.attributes[name][:type] == :set
70
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
71
+ end
72
+
73
+ builder.add_value(name, value)
74
+ end
75
+
76
+ @item_updater.attributes_to_delete.each do |name, value|
77
+ # The DELETE section in UpdateExpressions requires values to be a
78
+ # set to update a set attribute.
79
+ # Allow specifying values as any Enumerable collection (e.g. Array).
80
+ # Allow a single value not wrapped into a Set
81
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
82
+
83
+ builder.delete_value(name, value)
84
+ end
85
+ end
86
+
87
+ # require primary key to exist
88
+ condition_expression = "attribute_exists(#{@model_class.hash_key})"
89
+ if @model_class.range_key?
90
+ condition_expression += " AND attribute_exists(#{@model_class.range_key})"
91
+ end
92
+ builder.condition_expression = condition_expression
93
+
94
+ builder.request
95
+ end
96
+
97
+ private
98
+
99
+ def validate_primary_key!
100
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
101
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
102
+ end
103
+
104
+ def add_timestamps(attributes, skip_created_at: false)
105
+ return attributes unless @model_class.timestamps_enabled?
106
+
107
+ result = attributes.clone
108
+ timestamp = DateTime.now.in_time_zone(Time.zone)
109
+ result[:created_at] ||= timestamp unless skip_created_at
110
+ result[:updated_at] ||= timestamp
111
+ result
112
+ end
113
+
114
+ def dump_attribute(name, value)
115
+ options = @model_class.attributes[name]
116
+ Dumping.dump_field(value, options)
117
+ end
118
+
119
+ class UpdateRequestBuilder
120
+ attr_writer :hash_key, :range_key, :condition_expression
121
+
122
+ def initialize(model_class)
123
+ @model_class = model_class
124
+
125
+ @attributes_to_set = {}
126
+ @attributes_to_add = {}
127
+ @attributes_to_delete = {}
128
+ @attributes_to_remove = []
129
+ @condition_expression = nil
130
+ end
131
+
132
+ def set_attributes(attributes) # rubocop:disable Naming/AccessorMethodName
133
+ @attributes_to_set.merge!(attributes)
134
+ end
135
+
136
+ def add_value(name, value)
137
+ @attributes_to_add[name] = value
138
+ end
139
+
140
+ def delete_value(name, value)
141
+ @attributes_to_delete[name] = value
142
+ end
143
+
144
+ def remove_attributes(names)
145
+ @attributes_to_remove.concat(names)
146
+ end
147
+
148
+ def request
149
+ key = { @model_class.hash_key => @hash_key }
150
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
151
+
152
+ # Build UpdateExpression and keep names and values placeholders mapping
153
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
154
+ update_expression_statements = []
155
+ expression_attribute_names = {}
156
+ expression_attribute_values = {}
157
+ name_placeholder = '#_n0'
158
+ value_placeholder = ':_v0'
159
+
160
+ unless @attributes_to_set.empty?
161
+ statements = []
162
+
163
+ @attributes_to_set.each do |name, value|
164
+ statements << "#{name_placeholder} = #{value_placeholder}"
165
+
166
+ expression_attribute_names[name_placeholder] = name
167
+ expression_attribute_values[value_placeholder] = value
168
+
169
+ name_placeholder = name_placeholder.succ
170
+ value_placeholder = value_placeholder.succ
171
+ end
172
+
173
+ update_expression_statements << "SET #{statements.join(', ')}"
174
+ end
175
+
176
+ unless @attributes_to_add.empty?
177
+ statements = []
178
+
179
+ @attributes_to_add.each do |name, value|
180
+ statements << "#{name_placeholder} #{value_placeholder}"
181
+
182
+ expression_attribute_names[name_placeholder] = name
183
+ expression_attribute_values[value_placeholder] = value
184
+
185
+ name_placeholder = name_placeholder.succ
186
+ value_placeholder = value_placeholder.succ
187
+ end
188
+
189
+ update_expression_statements << "ADD #{statements.join(', ')}"
190
+ end
191
+
192
+ unless @attributes_to_delete.empty?
193
+ statements = []
194
+
195
+ @attributes_to_delete.each do |name, value|
196
+ statements << "#{name_placeholder} #{value_placeholder}"
197
+
198
+ expression_attribute_names[name_placeholder] = name
199
+ expression_attribute_values[value_placeholder] = value
200
+
201
+ name_placeholder = name_placeholder.succ
202
+ value_placeholder = value_placeholder.succ
203
+ end
204
+
205
+ update_expression_statements << "DELETE #{statements.join(', ')}"
206
+ end
207
+
208
+ unless @attributes_to_remove.empty?
209
+ name_placeholders = []
210
+
211
+ @attributes_to_remove.each do |name|
212
+ name_placeholders << name_placeholder
213
+
214
+ expression_attribute_names[name_placeholder] = name
215
+
216
+ name_placeholder = name_placeholder.succ
217
+ value_placeholder = value_placeholder.succ
218
+ end
219
+
220
+ update_expression_statements << "REMOVE #{name_placeholders.join(', ')}"
221
+ end
222
+
223
+ update_expression = update_expression_statements.join(' ')
224
+
225
+ {
226
+ update: {
227
+ key: key,
228
+ table_name: @model_class.table_name,
229
+ update_expression: update_expression,
230
+ expression_attribute_names: expression_attribute_names,
231
+ expression_attribute_values: expression_attribute_values,
232
+ condition_expression: @condition_expression
233
+ }
234
+ }
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'dynamoid/persistence/update_validations'
5
+
6
+ module Dynamoid
7
+ class TransactionWrite
8
+ class Upsert < Base
9
+ def initialize(model_class, hash_key, range_key, attributes)
10
+ super()
11
+
12
+ @model_class = model_class
13
+ @hash_key = hash_key
14
+ @range_key = range_key
15
+ @attributes = attributes
16
+ end
17
+
18
+ def on_registration
19
+ validate_primary_key!
20
+ Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
+ end
22
+
23
+ def on_commit; end
24
+
25
+ def on_rollback; end
26
+
27
+ def aborted?
28
+ false
29
+ end
30
+
31
+ def skipped?
32
+ attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
33
+ attributes_to_assign.empty? && !@model_class.timestamps_enabled?
34
+ end
35
+
36
+ def observable_by_user_result
37
+ nil
38
+ end
39
+
40
+ def action_request
41
+ # changed attributes to persist
42
+ changes = @attributes.dup
43
+ changes = add_timestamps(changes, skip_created_at: true)
44
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
45
+
46
+ # primary key to look up an item to update
47
+ partition_key_dumped = dump(@model_class.hash_key, @hash_key)
48
+ key = { @model_class.hash_key => partition_key_dumped }
49
+
50
+ if @model_class.range_key?
51
+ sort_key_dumped = dump(@model_class.range_key, @range_key)
52
+ key[@model_class.range_key] = sort_key_dumped
53
+ end
54
+
55
+ # Build UpdateExpression and keep names and values placeholders mapping
56
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
57
+ update_expression_statements = []
58
+ expression_attribute_names = {}
59
+ expression_attribute_values = {}
60
+
61
+ changes_dumped.each_with_index do |(name, value), i|
62
+ name_placeholder = "#_n#{i}"
63
+ value_placeholder = ":_s#{i}"
64
+
65
+ update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
66
+ expression_attribute_names[name_placeholder] = name
67
+ expression_attribute_values[value_placeholder] = value
68
+ end
69
+
70
+ update_expression = "SET #{update_expression_statements.join(', ')}"
71
+
72
+ {
73
+ update: {
74
+ key: key,
75
+ table_name: @model_class.table_name,
76
+ update_expression: update_expression,
77
+ expression_attribute_names: expression_attribute_names,
78
+ expression_attribute_values: expression_attribute_values
79
+ }
80
+ }
81
+ end
82
+
83
+ private
84
+
85
+ def validate_primary_key!
86
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
87
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
88
+ end
89
+
90
+ def add_timestamps(attributes, skip_created_at: false)
91
+ return attributes unless @model_class.timestamps_enabled?
92
+
93
+ result = attributes.clone
94
+ timestamp = DateTime.now.in_time_zone(Time.zone)
95
+ result[:created_at] ||= timestamp unless skip_created_at
96
+ result[:updated_at] ||= timestamp
97
+ result
98
+ end
99
+
100
+ def dump(name, value)
101
+ options = @model_class.attributes[name]
102
+ Dumping.dump_field(value, options)
103
+ end
104
+ end
105
+ end
106
+ end