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,673 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid/transaction_write/create'
4
+ require 'dynamoid/transaction_write/delete_with_primary_key'
5
+ require 'dynamoid/transaction_write/delete_with_instance'
6
+ require 'dynamoid/transaction_write/destroy'
7
+ require 'dynamoid/transaction_write/save'
8
+ require 'dynamoid/transaction_write/update_fields'
9
+ require 'dynamoid/transaction_write/update_attributes'
10
+ require 'dynamoid/transaction_write/upsert'
11
+ require 'dynamoid/transaction_write/item_updater'
12
+
13
+ module Dynamoid
14
+ # The class +TransactionWrite+ provides means to perform multiple modifying
15
+ # operations in transaction, that is atomically, so that either all of them
16
+ # succeed, or all of them fail.
17
+ #
18
+ # The persisting methods are supposed to be as close as possible to their
19
+ # non-transactional counterparts like +.create+, +#save+ and +#delete+:
20
+ #
21
+ # user = User.new()
22
+ # payment = Payment.find(1)
23
+ #
24
+ # Dynamoid::TransactionWrite.execute do |t|
25
+ # t.save! user
26
+ # t.create! Account, name: 'A'
27
+ # t.delete payment
28
+ # end
29
+ #
30
+ # The only difference is that the methods are called on a transaction
31
+ # instance and a model or a model class should be specified.
32
+ #
33
+ # So +user.save!+ becomes +t.save!(user)+, +Account.create!(name: 'A')+
34
+ # becomes +t.create!(Account, name: 'A')+, and +payment.delete+ becomes
35
+ # +t.delete(payment)+.
36
+ #
37
+ # A transaction can be used without a block. This way a transaction instance
38
+ # should be instantiated and committed manually with +#commit+ method:
39
+ #
40
+ # t = Dynamoid::TransactionWrite.new
41
+ #
42
+ # t.save! user
43
+ # t.create! Account, name: 'A'
44
+ # t.delete payment
45
+ #
46
+ # t.commit
47
+ #
48
+ # Some persisting methods are intentionally not available in a transaction,
49
+ # e.g. +.update+ and +.update!+ that simply call +.find+ and
50
+ # +#update_attributes+ methods. These methods perform multiple operations so
51
+ # cannot be implemented in a transactional atomic way.
52
+ #
53
+ #
54
+ # ### DynamoDB's transactions
55
+ #
56
+ # The main difference between DynamoDB transactions and a common interface is
57
+ # that DynamoDB's transactions are executed in batch. So in Dynamoid no
58
+ # changes are actually persisted when some transactional method (e.g+ `#save+) is
59
+ # called. All the changes are persisted at the end.
60
+ #
61
+ # A +TransactWriteItems+ DynamoDB operation is used (see
62
+ # [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html)
63
+ # for details).
64
+ #
65
+ #
66
+ # ### Callbacks
67
+ #
68
+ # The transactional methods support +before_+, +after_+ and +around_+
69
+ # callbacks to the extend the non-transactional methods support them.
70
+ #
71
+ # There is important difference - a transactional method runs callbacks
72
+ # immediately (even +after_+ ones) when it is called before changes are
73
+ # actually persisted. So code in +after_+ callbacks does not see observes
74
+ # them in DynamoDB and so for.
75
+ #
76
+ # When a callback aborts persisting of a model or a model is invalid then
77
+ # transaction is not aborted and may commit successfully.
78
+ #
79
+ #
80
+ # ### Transaction rollback
81
+ #
82
+ # A transaction is rolled back on DynamoDB's side automatically when:
83
+ # - an ongoing operation is in the process of updating the same item.
84
+ # - there is insufficient provisioned capacity for the transaction to be completed.
85
+ # - an item size becomes too large (bigger than 400 KB), a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
86
+ # - the aggregate size of the items in the transaction exceeds 4 MB.
87
+ # - there is a user error, such as an invalid data format.
88
+ #
89
+ # A transaction can be interrupted simply by an exception raised within a
90
+ # block. As far as no changes are actually persisted before the +#commit+
91
+ # method call - there is nothing to undo on the DynamoDB's site.
92
+ #
93
+ # Raising +Dynamoid::Errors::Rollback+ exception leads to interrupting a
94
+ # transation and it isn't propogated:
95
+ #
96
+ # Dynamoid::TransactionWrite.execute do |t|
97
+ # t.save! user
98
+ # t.create! Account, name: 'A'
99
+ #
100
+ # if user.is_admin?
101
+ # raise Dynamoid::Errors::Rollback
102
+ # end
103
+ # end
104
+ #
105
+ # When a transaction is successfully committed or rolled backed -
106
+ # corresponding +#after_commit+ or +#after_rollback+ callbacks are run for
107
+ # each involved model.
108
+ class TransactionWrite
109
+ def self.execute
110
+ transaction = new
111
+
112
+ begin
113
+ yield transaction
114
+ rescue StandardError => e
115
+ transaction.rollback
116
+
117
+ unless e.is_a?(Dynamoid::Errors::Rollback)
118
+ raise e
119
+ end
120
+ else
121
+ transaction.commit
122
+ end
123
+ end
124
+
125
+ def initialize
126
+ @actions = []
127
+ end
128
+
129
+ # Persist all the changes.
130
+ #
131
+ # transaction = Dynamoid::TransactionWrite.new
132
+ # # ...
133
+ # transaction.commit
134
+ def commit
135
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
136
+ return if actions_to_commit.empty?
137
+
138
+ action_requests = actions_to_commit.map(&:action_request)
139
+ Dynamoid.adapter.transact_write_items(action_requests)
140
+ actions_to_commit.each(&:on_commit)
141
+
142
+ nil
143
+ rescue Aws::Errors::ServiceError
144
+ run_on_rollback_callbacks
145
+ raise
146
+ end
147
+
148
+ def rollback
149
+ run_on_rollback_callbacks
150
+ end
151
+
152
+ # Create new model or persist changes in already existing one.
153
+ #
154
+ # Run the validation and callbacks. Returns +true+ if saving is successful
155
+ # and +false+ otherwise.
156
+ #
157
+ # user = User.new
158
+ #
159
+ # Dynamoid::TransactionWrite.execute do |t|
160
+ # t.save!(user)
161
+ # end
162
+ #
163
+ # Validation can be skipped with +validate: false+ option:
164
+ #
165
+ # user = User.new(age: -1)
166
+ #
167
+ # Dynamoid::TransactionWrite.execute do |t|
168
+ # t.save!(user, validate: false)
169
+ # end
170
+ #
171
+ # +save!+ by default sets timestamps attributes - +created_at+ and
172
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
173
+ # when updates already existing one.
174
+ #
175
+ # If a model is new and hash key (+id+ by default) is not assigned yet
176
+ # it was assigned implicitly with random UUID value.
177
+ #
178
+ # When a model is not persisted - its id should have unique value.
179
+ # Otherwise a transaction will be rolled back.
180
+ #
181
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
182
+ # and a partition key has value +nil+ and raises
183
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
184
+ # value +nil+.
185
+ #
186
+ # There are the following differences between transactional and
187
+ # non-transactional +#save!+:
188
+ # - transactional +#save!+ doesn't support the +:touch+ option
189
+ # - transactional +#save!+ doesn't support +lock_version+ attribute so it
190
+ # will not be incremented and will not be checked to detect concurrent
191
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
192
+ # exception will not be raised
193
+ # - transactional +#save!+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
194
+ # at saving new model when primary key is already used. A generic
195
+ # +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
196
+ # - transactional +save!+ doesn't raise
197
+ # +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
198
+ # was concurrently deleted
199
+ # - a table isn't created lazily if it doesn't exist yet
200
+ #
201
+ # @param model [Dynamoid::Document] a model
202
+ # @param options [Hash] (optional)
203
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
204
+ # @return [true|false] Whether saving successful or not
205
+ def save!(model, **options)
206
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: true)
207
+ register_action action
208
+ end
209
+
210
+ # Create new model or persist changes in already existing one.
211
+ #
212
+ # Run the validation and callbacks. Raise
213
+ # +Dynamoid::Errors::DocumentNotValid+ unless this object is valid.
214
+ #
215
+ # user = User.new
216
+ #
217
+ # Dynamoid::TransactionWrite.execute do |t|
218
+ # t.save(user)
219
+ # end
220
+ #
221
+ # Validation can be skipped with +validate: false+ option:
222
+ #
223
+ # user = User.new(age: -1)
224
+ #
225
+ # Dynamoid::TransactionWrite.execute do |t|
226
+ # t.save(user, validate: false)
227
+ # end
228
+ #
229
+ # +save+ by default sets timestamps attributes - +created_at+ and
230
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
231
+ # when updates already existing one.
232
+ #
233
+ # If a model is new and hash key (+id+ by default) is not assigned yet
234
+ # it was assigned implicitly with random UUID value.
235
+ #
236
+ # When a model is not persisted - its id should have unique value.
237
+ # Otherwise a transaction will be rolled back.
238
+ #
239
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
240
+ # and a partition key has value +nil+ and raises
241
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
242
+ # value +nil+.
243
+ #
244
+ # There are the following differences between transactional and
245
+ # non-transactional +#save+:
246
+ # - transactional +#save+ doesn't support the +:touch+ option
247
+ # - transactional +#save+ doesn't support +lock_version+ attribute so it
248
+ # will not be incremented and will not be checked to detect concurrent
249
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
250
+ # exception will not be raised
251
+ # - transactional +#save+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
252
+ # at saving new model when primary key is already used. A generic
253
+ # +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
254
+ # - transactional +save+ doesn't raise +Dynamoid::Errors::StaleObjectError+
255
+ # when a model that is being updated was concurrently deleted
256
+ # - a table isn't created lazily if it doesn't exist yet
257
+ #
258
+ # @param model [Dynamoid::Document] a model
259
+ # @param options [Hash] (optional)
260
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
261
+ # @return [true|false] Whether saving successful or not
262
+ def save(model, **options)
263
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: false)
264
+ register_action action
265
+ end
266
+
267
+ # Create a model.
268
+ #
269
+ # Dynamoid::TransactionWrite.execute do |t|
270
+ # t.create!(User, name: 'A')
271
+ # end
272
+ #
273
+ # Accepts both Hash and Array of Hashes and can create several models.
274
+ #
275
+ # Dynamoid::TransactionWrite.execute do |t|
276
+ # t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
277
+ # end
278
+ #
279
+ # Instantiates a model and pass it into an optional block to set other
280
+ # attributes.
281
+ #
282
+ # Dynamoid::TransactionWrite.execute do |t|
283
+ # t.create!(User, name: 'A') do |user|
284
+ # user.initialize_roles
285
+ # end
286
+ # end
287
+ #
288
+ # Validates model and runs callbacks.
289
+ #
290
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but
291
+ # not specified or has value +nil+.
292
+ #
293
+ # There are the following differences between transactional and
294
+ # non-transactional +#create+:
295
+ # - transactional +#create!+ doesn't support +lock_version+ attribute so it
296
+ # will not be incremented and will not be checked to detect concurrent
297
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
298
+ # exception will not be raised
299
+ # - transactional +#create!+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
300
+ # at saving new model when primary key is already used. A generic
301
+ # +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
302
+ # - a table isn't created lazily if it doesn't exist yet
303
+ #
304
+ # @param model_class [Class] a model class which should be instantiated
305
+ # @param attributes [Hash|Array<Hash>] attributes of a model
306
+ # @param block [Proc] a block to process a model after initialization
307
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
308
+ def create!(model_class, attributes = {}, &block)
309
+ if attributes.is_a? Array
310
+ attributes.map do |attr|
311
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block)
312
+ register_action action
313
+ end
314
+ else
315
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block)
316
+ register_action action
317
+ end
318
+ end
319
+
320
+ # Create a model.
321
+ #
322
+ # Dynamoid::TransactionWrite.execute do |t|
323
+ # t.create(User, name: 'A')
324
+ # end
325
+ #
326
+ # Accepts both Hash and Array of Hashes and can create several models.
327
+ #
328
+ # Dynamoid::TransactionWrite.execute do |t|
329
+ # t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
330
+ # end
331
+ #
332
+ # Instantiates a model and pass it into an optional block to set other
333
+ # attributes.
334
+ #
335
+ # Dynamoid::TransactionWrite.execute do |t|
336
+ # t.create(User, name: 'A') do |user|
337
+ # user.initialize_roles
338
+ # end
339
+ # end
340
+ #
341
+ # Validates model and runs callbacks.
342
+ #
343
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but
344
+ # not specified or has value +nil+.
345
+ #
346
+ # There are the following differences between transactional and
347
+ # non-transactional +#create+:
348
+ # - transactional +#create+ doesn't support +lock_version+ attribute so it
349
+ # will not be incremented and will not be checked to detect concurrent
350
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
351
+ # exception will not be raised
352
+ # - transactional +#create+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
353
+ # at saving new model when primary key is already used. A generic
354
+ # +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
355
+ # - a table isn't created lazily if it doesn't exist yet
356
+ #
357
+ # @param model_class [Class] a model class which should be instantiated
358
+ # @param attributes [Hash|Array<Hash>] attributes of a model
359
+ # @param block [Proc] a block to process a model after initialization
360
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
361
+ def create(model_class, attributes = {}, &block)
362
+ if attributes.is_a? Array
363
+ attributes.map do |attr|
364
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block)
365
+ register_action action
366
+ end
367
+ else
368
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block)
369
+ register_action action
370
+ end
371
+ end
372
+
373
+ # Update an existing document or create a new one.
374
+ #
375
+ # If a document with specified hash and range keys doesn't exist it
376
+ # creates a new document with specified attributes. Doesn't run
377
+ # validations and callbacks.
378
+ #
379
+ # Dynamoid::TransactionWrite.execute do |t|
380
+ # t.upsert(User, '1', age: 26)
381
+ # end
382
+ #
383
+ # If range key is declared for a model it should be passed as well:
384
+ #
385
+ # Dynamoid::TransactionWrite.execute do |t|
386
+ # t.upsert(User, '1', 'Tylor', age: 26)
387
+ # end
388
+ #
389
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
390
+ # attributes is not declared in the model class.
391
+ #
392
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
393
+ # +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
394
+ # but has value +nil+.
395
+ #
396
+ # There are the following differences between transactional and
397
+ # non-transactional +#upsert+:
398
+ # - transactional +#upsert+ doesn't support conditions (that's +if+ and
399
+ # +unless_exists+ options)
400
+ # - transactional +#upsert+ doesn't return a document that was updated or
401
+ # created
402
+ #
403
+ # @param model_class [Class] a model class
404
+ # @param hash_key [Scalar value] hash key value
405
+ # @param range_key [Scalar value] range key value (optional)
406
+ # @param attributes [Hash]
407
+ # @return [nil]
408
+ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
409
+ action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes)
410
+ register_action action
411
+ end
412
+
413
+ # Update document.
414
+ #
415
+ # Doesn't run validations and callbacks.
416
+ #
417
+ # Dynamoid::TransactionWrite.execute do |t|
418
+ # t.update_fields(User, '1', age: 26)
419
+ # end
420
+ #
421
+ # If range key is declared for a model it should be passed as well:
422
+ #
423
+ # Dynamoid::TransactionWrite.execute do |t|
424
+ # t.update_fields(User, '1', 'Tylor', age: 26)
425
+ # end
426
+ #
427
+ # Updates can also be performed in a block.
428
+ #
429
+ # Dynamoid::TransactionWrite.execute do |t|
430
+ # t.update_fields(User, 1) do |u|
431
+ # u.add(article_count: 1)
432
+ # u.delete(favorite_colors: 'green')
433
+ # u.set(age: 27, last_name: 'Tylor')
434
+ # end
435
+ # end
436
+ #
437
+ # Operation +add+ just adds a value for numeric attributes and join
438
+ # collections if attribute is a set.
439
+ #
440
+ # t.update_fields(User, 1) do |u|
441
+ # u.add(age: 1, followers_count: 5)
442
+ # u.add(hobbies: ['skying', 'climbing'])
443
+ # end
444
+ #
445
+ # Operation +delete+ is applied to collection attribute types and
446
+ # substructs one collection from another.
447
+ #
448
+ # t.update_fields(User, 1) do |u|
449
+ # u.delete(hobbies: ['skying'])
450
+ # end
451
+ #
452
+ # Operation +set+ just changes an attribute value:
453
+ #
454
+ # t.update_fields(User, 1) do |u|
455
+ # u.set(age: 21)
456
+ # end
457
+ #
458
+ # Operation +remove+ removes one or more attributes from an item.
459
+ #
460
+ # t.update_fields(User, 1) do |u|
461
+ # u.remove(:age)
462
+ # end
463
+ #
464
+ # All the operations work like +ADD+, +DELETE+, +REMOVE+, and +SET+ actions supported
465
+ # by +UpdateExpression+
466
+ # {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html]
467
+ # of +UpdateItem+ operation.
468
+ #
469
+ # It's atomic operations. So adding or deleting elements in a collection
470
+ # or incrementing or decrementing a numeric field is atomic and does not
471
+ # interfere with other write requests.
472
+ #
473
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
474
+ # attributes is not declared in the model class.
475
+ #
476
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
477
+ # +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
478
+ # but has value +nil+.
479
+ #
480
+ # There are the following differences between transactional and
481
+ # non-transactional +#update_fields+:
482
+ # - transactional +#update_fields+ doesn't support conditions (that's +if+
483
+ # and +unless_exists+ options)
484
+ # - transactional +#update_fields+ doesn't return a document that was
485
+ # updated or created
486
+ #
487
+ # @param model_class [Class] a model class
488
+ # @param hash_key [Scalar value] hash key value
489
+ # @param range_key [Scalar value] range key value (optional)
490
+ # @param attributes [Hash]
491
+ # @return [nil]
492
+ def update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block)
493
+ # given no attributes, but there may be a block
494
+ if range_key.is_a?(Hash) && !attributes
495
+ attributes = range_key
496
+ range_key = nil
497
+ end
498
+
499
+ action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes, &block)
500
+ register_action action
501
+ end
502
+
503
+ # Update multiple attributes at once.
504
+ #
505
+ # Dynamoid::TransactionWrite.execute do |t|
506
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
507
+ # end
508
+ #
509
+ # Returns +true+ if saving is successful and +false+
510
+ # otherwise.
511
+ #
512
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
513
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
514
+ # required but has value +nil+.
515
+ #
516
+ # There are the following differences between transactional and
517
+ # non-transactional +#update_attributes+:
518
+ # - transactional +#update_attributes+ doesn't support +lock_version+ attribute so it
519
+ # will not be incremented and will not be checked to detect concurrent
520
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
521
+ # exception will not be raised
522
+ # - transactional +update_attributes+ doesn't raise
523
+ # +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
524
+ # was concurrently deleted
525
+ # - a table isn't created lazily if it doesn't exist yet
526
+ #
527
+ # @param model [Dynamoid::Document] a model
528
+ # @param attributes [Hash] a hash of attributes to update
529
+ # @return [true|false] Whether updating successful or not
530
+ def update_attributes(model, attributes)
531
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false)
532
+ register_action action
533
+ end
534
+
535
+ # Update multiple attributes at once.
536
+ #
537
+ # Returns +true+ if saving is successful and +false+
538
+ # otherwise.
539
+ #
540
+ # Dynamoid::TransactionWrite.execute do |t|
541
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
542
+ # end
543
+ #
544
+ # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
545
+ # fails.
546
+ #
547
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
548
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
549
+ # required but has value +nil+.
550
+ #
551
+ # There are the following differences between transactional and
552
+ # non-transactional +#update_attributes!+:
553
+ # - transactional +#update_attributes!+ doesn't support +lock_version+
554
+ # attribute so it will not be incremented and will not be checked to detect
555
+ # concurrent modification of a model and
556
+ # +Dynamoid::Errors::StaleObjectError+ exception will not be raised
557
+ # - transactional +update_attributes!+ doesn't raise
558
+ # +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
559
+ # was concurrently deleted
560
+ # - a table isn't created lazily if it doesn't exist yet
561
+ #
562
+ # @param model [Dynamoid::Document] a model
563
+ # @param attributes [Hash] a hash of attributes to update
564
+ def update_attributes!(model, attributes)
565
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true)
566
+ register_action action
567
+ end
568
+
569
+ # Delete a model.
570
+ #
571
+ # Can be called either with a model:
572
+ #
573
+ # Dynamoid::TransactionWrite.execute do |t|
574
+ # t.delete(user)
575
+ # end
576
+ #
577
+ # or with a primary key:
578
+ #
579
+ # Dynamoid::TransactionWrite.execute do |t|
580
+ # t.delete(User, user_id)
581
+ # end
582
+ #
583
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
584
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
585
+ # required but has value +nil+.
586
+ #
587
+ # There are the following differences between transactional and
588
+ # non-transactional +#delete+: TBD
589
+ # - transactional +#delete+ doesn't support +lock_version+ attribute so it
590
+ # will not be incremented and will not be checked to detect concurrent
591
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
592
+ # exception will not be raised
593
+ # - transactional +#delete+ doesn't disassociate a model from associated ones
594
+ # if there is any
595
+ #
596
+ # @param model_or_model_class [Class|Dynamoid::Document] either model or model class
597
+ # @param hash_key [Scalar value] hash key value
598
+ # @param range_key [Scalar value] range key value (optional)
599
+ # @return [Dynamoid::Document] self
600
+ def delete(model_or_model_class, hash_key = nil, range_key = nil)
601
+ action = if model_or_model_class.is_a? Class
602
+ Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key)
603
+ else
604
+ Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class)
605
+ end
606
+ register_action action
607
+ end
608
+
609
+ # Delete a model.
610
+ #
611
+ # Runs callbacks.
612
+ #
613
+ # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
614
+ # failed (e.g. aborted by a callback).
615
+ #
616
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
617
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
618
+ # required but has value +nil+.
619
+ #
620
+ # There are the following differences between transactional and
621
+ # non-transactional +#destroy!+:
622
+ # - transactional +#destroy!+ doesn't support +lock_version+ attribute so it
623
+ # will not be incremented and will not be checked to detect concurrent
624
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
625
+ # exception will not be raised
626
+ # - transactional +#destroy!+ doesn't disassociate a model from associated ones
627
+ # if there are association declared in the model class
628
+ #
629
+ # @param model [Dynamoid::Document] a model
630
+ # @return [Dynamoid::Document|false] returns self if destoying is succefull, +false+ otherwise
631
+ def destroy!(model)
632
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
633
+ register_action action
634
+ end
635
+
636
+ # Delete a model.
637
+ #
638
+ # Runs callbacks.
639
+ #
640
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
641
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
642
+ # required but has value +nil+.
643
+ #
644
+ # There are the following differences between transactional and
645
+ # non-transactional +#destroy+:
646
+ # - transactional +#destroy+ doesn't support +lock_version+ attribute so it
647
+ # will not be incremented and will not be checked to detect concurrent
648
+ # modification of a model and +Dynamoid::Errors::StaleObjectError+
649
+ # exception will not be raised
650
+ # - transactional +#destroy+ doesn't disassociate a model from associated ones
651
+ # if there are association declared in the model class
652
+ #
653
+ # @param model [Dynamoid::Document] a model
654
+ # @return [Dynamoid::Document] self
655
+ def destroy(model)
656
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false)
657
+ register_action action
658
+ end
659
+
660
+ private
661
+
662
+ def register_action(action)
663
+ @actions << action
664
+ action.on_registration
665
+ action.observable_by_user_result
666
+ end
667
+
668
+ def run_on_rollback_callbacks
669
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
670
+ actions_to_commit.each(&:on_rollback)
671
+ end
672
+ end
673
+ end
@@ -288,7 +288,9 @@ module Dynamoid
288
288
 
289
289
  class BinaryTypeCaster < Base
290
290
  def process(value)
291
- if value.is_a? String
291
+ if value.is_a?(StringIO) || value.is_a?(IO)
292
+ value
293
+ elsif value.is_a?(String)
292
294
  value.dup
293
295
  else
294
296
  value.to_s