dynamoid 3.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e13af88750cfc2b8421710068215b18cf596f86b0c5caa207574535af04369cc
4
- data.tar.gz: b0cc179fb6c547fd766c788300bfe8fc38a9e10117895c0b39b665fcc609e448
3
+ metadata.gz: f3e330233bdd01c0bb962fb7253b48629fe9f0aeb1ecdcb6cfe7d3272ddddbe0
4
+ data.tar.gz: 7f6ee27e8d6dedb5fe21bce21a6c00397f42d85dfa40b8d30dae4e13a8294d15
5
5
  SHA512:
6
- metadata.gz: 3b363e83838f36600b1af31c59406ac4fccad981e24af8762d5ae73fd1b648a2d03754483be89ed648f7ca982dc1640901af242c0b626ef9fb34b3002438edb4
7
- data.tar.gz: a1bc08e5ab754f0a2ee383d2ac335396e7e058ad3b91df2d7c532d346da79daa0d239053ab856a8dfd0a2bdfb213f70a654cecdd3b42901c0261e61bcaca9ad5
6
+ metadata.gz: f84a868794dd6b812ccd6936eaa16c91a8d0f87b2ae959b89a8b01802537490168449bfa83f62d92f8643a6c47043cefeb9168da3301ca4f005400bc1086406f
7
+ data.tar.gz: 528587c73879f49d25e660974fa5832464eed5773da06535a76bfe3c9c96311a1877eb801e68362c94eadc479cf65a8df053a0f47574c78fbf5919859cb88e66
data/CHANGELOG.md CHANGED
@@ -11,21 +11,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ### Changed
12
12
  ### Removed
13
13
 
14
- ## 3.11.0
14
+ ## 3.12.0
15
15
 
16
+ ### Fixed
17
+ * [#849](https://github.com/Dynamoid/dynamoid/pull/849) Fixed saving a field of custom type when it implements both `.dynamoid_dump()` and `#dynamoid_dump` method and use the former one.
18
+ A proper behaviour is to use adapter's .dynamoid_dump method instead of a value's #dynamoid_dump method.
19
+ * [#851](https://github.com/Dynamoid/dynamoid/pull/851) Fixed Dirty API and don't require custom types to implement `#==` method. Add field option `:comparable`.
20
+ * [#913](https://github.com/Dynamoid/dynamoid/pull/913) Fixed saving partition keys of Dynamoid-specific types (e.g. `date` or `datetime`) and always convert them into a proper DynamoDB type
21
+ * [#917](https://github.com/Dynamoid/dynamoid/pull/917) Fixed transactional write operations when primary key is of non-native DynamoDB type
22
+ * [#927](https://github.com/Dynamoid/dynamoid/pull/927) Multiple fixes in transactional and non-transactional write methods and finders:
23
+ * Changed non-transactional method `#find` and raise `MissingHashKey` when a given partition key is `nil`
24
+ * Fixed non-transactional method `#save!` and raise `RecordNotSaved` when a callback throws `:abort` and terminates the process
25
+ * Check whether partition and sort key are specified in the non-transactional persistence methods and raise a proper exception (`MissingHashKey` or `MissingRangeKey`)
26
+ * Fixed methods `#inc`, `#increment!` and `#decrement!`, `#update` and `#update!` to not create a new item when an item with specified primary key is already deleted
27
+ * Fixed method `#update_attribute` to not raise `StaleObjectError` when an item with specified primary key is already deleted
28
+ * Fixed method `#where` with condition on a `:serialized` field to not raise an exception
29
+ ### Added
30
+ * [#910](https://github.com/Dynamoid/dynamoid/pull/910) Added `key_type` option to the `table` method to change type of a partition key field (@ogidow)
31
+ * [#916](https://github.com/Dynamoid/dynamoid/pull/916) Added new `error_on_scan` config option to raises an error and prevent table scans (DynamoDB's `Scan` operation) (@aaronalberg)
32
+ * [#926](https://github.com/Dynamoid/dynamoid/pull/926) Implemented read transactions (using DynamoDB's `TransactGetItems` operation)
33
+ ### Changed
34
+ * [#846](https://github.com/Dynamoid/dynamoid/pull/846) Changed transactional `update_fields()` method to accept a block and support low-level operations `set`, `add`, `delete`, `remove` (similar to the non-transactional `#update()` method) (@ckhsponge)
35
+ ### Removed
36
+
37
+ ## 3.11.0 / 2025-01-12
16
38
  ### Fixed
17
39
  * [#829](https://github.com/Dynamoid/dynamoid/pull/829) Fixed saving of in-place field changes
18
40
  * [#812](https://github.com/Dynamoid/dynamoid/pull/812) Restored sanitizing of attribute names in `#where` conditions
19
41
  * [#721](https://github.com/Dynamoid/dynamoid/pull/721) Fixed code examples in README.md (@ndjndj)
20
42
  ### Added
21
- * [#688](https://github.com/Dynamoid/dynamoid/pull/688) Transactional modifying was added utilizing `TransactWriteItems` operation (@ckhsponge)
43
+ * [#688](https://github.com/Dynamoid/dynamoid/pull/688) Implemented write transactions (using DynamoDB's `TransactWriteItems` operation) (@ckhsponge)
22
44
  * [#794](https://github.com/Dynamoid/dynamoid/pull/794) Added new config option `store_empty_string_as_nil`
23
45
  * [#828](https://github.com/Dynamoid/dynamoid/pull/828) Added Ruby 3.4, Rails 8.0 and Rails 7.2 in CI
24
46
  ### Changed
25
47
  * [#832](https://github.com/Dynamoid/dynamoid/pull/832) Support String condition expressions with `#where`
26
48
  * [#822](https://github.com/Dynamoid/dynamoid/pull/822) Support binary type natively. Also added new config option `store_binary_as_native` (@dalibor)
27
49
 
28
- ## 3.10.0
50
+ ## 3.10.0 / 2024-02-10
29
51
  ### Fixed
30
52
  * [#681](https://github.com/Dynamoid/dynamoid/pull/681) Fixed saving persisted model and deleting attributes with `nil` value if `config.store_attribute_with_nil_value` is `false`
31
53
  * [#716](https://github.com/Dynamoid/dynamoid/pull/716), [#691](https://github.com/Dynamoid/dynamoid/pull/691), [#687](https://github.com/Dynamoid/dynamoid/pull/687), [#660](https://github.com/Dynamoid/dynamoid/pull/660) Numerous fixes in README.md and RDoc documentation (@ndjndj, @kiharito, @dunkOnIT)
data/README.md CHANGED
@@ -24,8 +24,8 @@ DynamoDB is not like other document-based databases you might know, and
24
24
  is very different indeed from relational databases. It sacrifices
25
25
  anything beyond the simplest relational queries and transactional
26
26
  support to provide a fast, cost-efficient, and highly durable storage
27
- solution. If your database requires complicated relational queries and
28
- transaction support, then this modest Gem cannot provide them for you,
27
+ solution. If your database requires complicated relational queries
28
+ then this modest Gem cannot provide them for you
29
29
  and neither can DynamoDB. In those cases you would do better to look
30
30
  elsewhere for your database needs.
31
31
 
@@ -102,8 +102,8 @@ credentials = Aws::AssumeRoleCredentials.new(
102
102
  )
103
103
 
104
104
  Dynamoid.configure do |config|
105
- config.region = 'us-west-2',
106
- config.credentials = credentials
105
+ config.region = 'us-west-2'
106
+ config.credentials = credentials
107
107
  end
108
108
  ```
109
109
 
@@ -132,8 +132,8 @@ end
132
132
  Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
133
133
 
134
134
  Its compatibility is tested against following Ruby versions: 2.3, 2.4,
135
- 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and 3.3, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
136
- 5.2, 6.0, 6.1, 7.0 and 7.1.
135
+ 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, and 3.4, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
136
+ 5.2, 6.0, 6.1, 7.0, 7.1, 7.2, and 8.0.
137
137
 
138
138
  ## Setup
139
139
 
@@ -471,6 +471,27 @@ method, which would return either `:string` or `:number`.
471
471
  DynamoDB may support some other attribute types that are not yet
472
472
  supported by Dynamoid.
473
473
 
474
+ If a custom type implements `#==` method you can specify `comparable:
475
+ true` option in a field declaration to specify that an object is safely
476
+ comparable for the purpose of detecting changes. By default old and new
477
+ objects will be compared by their serialized representation.
478
+
479
+ ```ruby
480
+ class Money
481
+ # ...
482
+
483
+ def ==(other)
484
+ # comparison logic
485
+ end
486
+ end
487
+
488
+ class User
489
+ # ...
490
+
491
+ field :balance, Money, comparable: true
492
+ end
493
+ ```
494
+
474
495
  ### Sort key
475
496
 
476
497
  Along with partition key table may have a sort key. In order to declare
@@ -620,6 +641,7 @@ with `inheritance_field` table option:
620
641
  ```ruby
621
642
  class Car
622
643
  include Dynamoid::Document
644
+
623
645
  table inheritance_field: :my_new_type
624
646
 
625
647
  field :my_new_type
@@ -914,6 +936,7 @@ It could be done with `project` method:
914
936
  ```ruby
915
937
  class User
916
938
  include Dynamoid::Document
939
+
917
940
  field :name
918
941
  end
919
942
 
@@ -1091,6 +1114,14 @@ on the base table*
1091
1114
  > Please note that this API is experimental and can be changed in
1092
1115
  > future releases.
1093
1116
 
1117
+ DynamoDB supports modifying and reading operations but there are some
1118
+ limitations:
1119
+ - read and write operation cannot be combined in the same transaction
1120
+ - operations are executed in batch, so operations should be given before
1121
+ actual execution and cannot be changed on the fly
1122
+
1123
+ #### Modifying transactions
1124
+
1094
1125
  Multiple modifying actions can be grouped together and submitted as an
1095
1126
  all-or-nothing operation. Atomic modifying operations are supported in
1096
1127
  Dynamoid using transactions. If any action in the transaction fails they
@@ -1110,8 +1141,7 @@ model
1110
1141
  These methods are supposed to behave exactly like their
1111
1142
  non-transactional counterparts.
1112
1143
 
1113
-
1114
- #### Create models
1144
+ ##### Create models
1115
1145
 
1116
1146
  Models can be created inside of a transaction. The partition and sort
1117
1147
  keys, if applicable, are used to determine uniqueness. Creating will
@@ -1135,7 +1165,7 @@ Dynamoid::TransactionWrite.execute do |txn|
1135
1165
  end
1136
1166
  ```
1137
1167
 
1138
- #### Save models
1168
+ ##### Save models
1139
1169
 
1140
1170
  Models can be saved in a transaction. New records are created otherwise
1141
1171
  the model is updated. Save, create, update, validate and destroy
@@ -1154,7 +1184,7 @@ Dynamoid::TransactionWrite.execute do |txn|
1154
1184
  end
1155
1185
  ```
1156
1186
 
1157
- #### Update models
1187
+ ##### Update models
1158
1188
 
1159
1189
  A model can be updated by providing a model or primary key, and the fields to update.
1160
1190
 
@@ -1166,10 +1196,23 @@ Dynamoid::TransactionWrite.execute do |txn|
1166
1196
  # sets the name and title for a user
1167
1197
  # The user is found by id (that equals 1)
1168
1198
  txn.update_fields(User, '1', name: 'bob', title: 'mister')
1199
+
1200
+ # sets the name, increments a count and deletes a field
1201
+ txn.update_fields(User, 1) do |t|
1202
+ t.set(name: 'bob')
1203
+ t.add(article_count: 1)
1204
+ t.delete(:title)
1205
+ end
1206
+
1207
+ # adds to a set of integers and deletes from a set of strings
1208
+ txn.update_fields(User, 2) do |t|
1209
+ t.add(friend_ids: [1, 2])
1210
+ t.delete(child_names: ['bebe'])
1211
+ end
1169
1212
  end
1170
1213
  ```
1171
1214
 
1172
- #### Destroy or delete models
1215
+ ##### Destroy or delete models
1173
1216
 
1174
1217
  Models can be used or the model class and key can be specified.
1175
1218
  `#destroy` uses callbacks and validations. Use `#delete` to skip
@@ -1188,7 +1231,7 @@ Dynamoid::TransactionWrite.execute do |txn|
1188
1231
  end
1189
1232
  ```
1190
1233
 
1191
- #### Validation failures that don't raise
1234
+ ##### Validation failures that don't raise
1192
1235
 
1193
1236
  All of the transaction methods can be called without the `!` which
1194
1237
  results in `false` instead of a raised exception when validation fails.
@@ -1208,7 +1251,7 @@ Dynamoid::TransactionWrite.execute do |txn|
1208
1251
  end
1209
1252
  ```
1210
1253
 
1211
- #### Incrementally building a transaction
1254
+ ##### Incrementally building a transaction
1212
1255
 
1213
1256
  Transactions can also be built without a block.
1214
1257
 
@@ -1222,6 +1265,41 @@ transaction.upsert(Address, 'A#1', street: '123')
1222
1265
  transaction.commit # changes are persisted in this moment
1223
1266
  ```
1224
1267
 
1268
+ #### Reading transactions
1269
+
1270
+ Multiple reading actions can be grouped together and submitted as an
1271
+ all-or-nothing operation. Atomic operations are supported in Dynamoid
1272
+ using transactions. If any action in the transaction fails they all
1273
+ fail.
1274
+
1275
+ The following actions are supported:
1276
+
1277
+ * `#find` - load a single model or multiple models by its primary key
1278
+
1279
+ These methods are supposed to behave exactly like their
1280
+ non-transactional counterparts.
1281
+
1282
+ ##### Find a model
1283
+
1284
+ The `#find` action can load single model or multiple ones. Different
1285
+ model classes can be mixed in the same transactions. Result is returned
1286
+ as a plain list of all the found models. The order is preserved.
1287
+
1288
+ ```ruby
1289
+ user, address = Dynamoid::TransactionRead.execute do |t|
1290
+ t.find(User, user_id)
1291
+ t.find(Address, address_id)
1292
+ end
1293
+ ```
1294
+
1295
+ Multiple primary keys can be specified at once:
1296
+
1297
+ ```ruby
1298
+ users = Dynamoid::TransactionRead.execute do |t|
1299
+ t.find(User, [id1, id2, id3])
1300
+ end
1301
+ ```
1302
+
1225
1303
  ### PartiQL
1226
1304
 
1227
1305
  To run PartiQL statements `Dynamoid.adapter.execute` method should be
@@ -1269,6 +1347,7 @@ Listed below are all configuration options.
1269
1347
  * `write_capacity` - is used at table or indices creation. Default is 20
1270
1348
  (units)
1271
1349
  * `warn_on_scan` - log warnings when scan table. Default is `true`
1350
+ * `error_on_scan` - raises an error when scan table. Default is `false`
1272
1351
  * `endpoint` - if provided, it communicates with the DynamoDB listening
1273
1352
  at the endpoint. This is useful for testing with
1274
1353
  [DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html)
@@ -1501,6 +1580,7 @@ order to troubleshoot and debug issues just set it:
1501
1580
  ```ruby
1502
1581
  class User
1503
1582
  include Dynamoid::Document
1583
+
1504
1584
  field name
1505
1585
  end
1506
1586
 
@@ -294,6 +294,14 @@ module Dynamoid
294
294
  Transact.new(client).transact_write_items(items)
295
295
  end
296
296
 
297
+ def transact_read_items(items)
298
+ request = {
299
+ transact_items: items,
300
+ return_consumed_capacity: 'TOTAL',
301
+ }
302
+ client.transact_get_items(request)
303
+ end
304
+
297
305
  # Create a table on DynamoDB. This usually takes a long time to complete.
298
306
  #
299
307
  # @param [String] table_name the name of the table to create
@@ -36,6 +36,7 @@ module Dynamoid
36
36
  option :read_capacity, default: 100
37
37
  option :write_capacity, default: 20
38
38
  option :warn_on_scan, default: true
39
+ option :error_on_scan, default: false
39
40
  option :endpoint, default: nil
40
41
  option :identity_map, default: false
41
42
  option :timestamps, default: true
@@ -563,7 +563,7 @@ module Dynamoid
563
563
  if @key_fields_detector.key_present?
564
564
  raw_pages_via_query
565
565
  else
566
- issue_scan_warning if Dynamoid::Config.warn_on_scan && !@where_conditions.empty?
566
+ validate_scan_conditions
567
567
  raw_pages_via_scan
568
568
  end
569
569
  end
@@ -598,6 +598,12 @@ module Dynamoid
598
598
  end
599
599
  end
600
600
 
601
+ def validate_scan_conditions
602
+ raise Dynamoid::Errors::ScanProhibited if Dynamoid::Config.error_on_scan && !@where_conditions.empty?
603
+
604
+ issue_scan_warning if Dynamoid::Config.warn_on_scan && !@where_conditions.empty?
605
+ end
606
+
601
607
  def issue_scan_warning
602
608
  Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
603
609
  Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.underscore}.rb:"
@@ -697,11 +703,13 @@ module Dynamoid
697
703
  # e.g. for NULL operator value should be boolean
698
704
  # and isn't related to an attribute own type
699
705
  def type_cast_condition_parameter(key, value)
700
- return value if %i[array set].include?(source.attributes[key.to_sym][:type])
706
+ field_type = source.attributes[key.to_sym][:type]
707
+
708
+ return value if %i[array set].include?(field_type)
701
709
 
702
710
  if [true, false].include?(value) # Support argument for null/not_null operators
703
711
  value
704
- elsif !value.respond_to?(:to_ary)
712
+ elsif !value.respond_to?(:to_ary) || field_type == :serialized
705
713
  options = source.attributes[key.to_sym]
706
714
  value_casted = TypeCasting.cast_field(value, options)
707
715
  Dumping.dump_field(value_casted, options)
@@ -301,7 +301,19 @@ module Dynamoid
301
301
  return false if value_from_database.nil?
302
302
 
303
303
  value = read_attribute(name)
304
- value != value_from_database
304
+ type_options = self.class.attributes[name.to_sym]
305
+
306
+ unless type_options[:type].is_a?(Class) && !type_options[:comparable]
307
+ # common case
308
+ value != value_from_database
309
+ else
310
+ # objects of a custom type that does not implement its own `#==` method
311
+ # (that's declared by `comparable: false` or just not specifying the
312
+ # option `comparable`) are compared by comparing their dumps
313
+ dump = Dumping.dump_field(value, type_options)
314
+ dump_from_database = Dumping.dump_field(value_from_database, type_options)
315
+ dump != dump_from_database
316
+ end
305
317
  end
306
318
 
307
319
  module DeepDupper
@@ -318,26 +330,25 @@ module Dynamoid
318
330
 
319
331
  case value
320
332
  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
333
+ # Till Ruby 2.4 these immutable objects could not be duplicated.
334
+ # IO objects (used for the binary type) cannot be duplicated as well.
323
335
  value
324
- when String
325
- value.dup
326
336
  when Array
327
- if of.is_a? Class # custom type
337
+ if of.is_a? Class
338
+ # custom type
328
339
  value.map { |e| dup_attribute(e, type: of) }
329
340
  else
330
341
  value.deep_dup
331
342
  end
332
343
  when Set
333
344
  Set.new(value.map { |e| dup_attribute(e, type: of) })
334
- when Hash
335
- value.deep_dup
336
345
  else
337
- if type.is_a? Class # custom type
338
- Marshal.load(Marshal.dump(value)) # dup instance variables
346
+ if type.is_a? Class
347
+ # custom type
348
+ dump = Dumping.dump_field(value, type_options)
349
+ Undumping.undump_field(dump.deep_dup, type_options)
339
350
  else
340
- value.dup # date, datetime
351
+ value.deep_dup
341
352
  end
342
353
  end
343
354
  end
@@ -323,10 +323,10 @@ module Dynamoid
323
323
  def process(value)
324
324
  field_class = @options[:type]
325
325
 
326
- if value.respond_to?(:dynamoid_dump)
327
- value.dynamoid_dump
328
- elsif field_class.respond_to?(:dynamoid_dump)
326
+ if field_class.respond_to?(:dynamoid_dump)
329
327
  field_class.dynamoid_dump(value)
328
+ elsif value.respond_to?(:dynamoid_dump)
329
+ value.dynamoid_dump
330
330
  else
331
331
  raise ArgumentError, "Neither #{field_class} nor #{value} supports serialization for Dynamoid."
332
332
  end
@@ -86,10 +86,25 @@ module Dynamoid
86
86
 
87
87
  class UnsupportedKeyType < Error; end
88
88
 
89
- class UnknownAttribute < Error; end
89
+ class UnknownAttribute < Error
90
+ attr_reader :model_class, :attribute_name
91
+
92
+ def initialize(model_class, attribute_name)
93
+ super("Attribute #{attribute_name} does not exist in #{model_class}")
94
+
95
+ @model_class = model_class
96
+ @attribute_name = attribute_name
97
+ end
98
+ end
90
99
 
91
100
  class SubclassNotFound < Error; end
92
101
 
93
102
  class Rollback < Error; end
103
+
104
+ class ScanProhibited < Error
105
+ def initialize(_msg = nil)
106
+ super('Scan operations prohibited. Modify Dynamoid::Config.error_on_scan to change this behavior.')
107
+ end
108
+ end
94
109
  end
95
110
  end
@@ -197,9 +197,19 @@ module Dynamoid
197
197
  # field :id, :integer
198
198
  # end
199
199
  #
200
+ # To declare a new attribute with not-default type as a table hash key a
201
+ # :key_type option can be used:
202
+ #
203
+ # class User
204
+ # include Dynamoid::Document
205
+ #
206
+ # table key: :user_id, key_type: :integer
207
+ # end
208
+ #
200
209
  # @param options [Hash] options to override default table settings
201
210
  # @option options [Symbol] :name name of a table
202
211
  # @option options [Symbol] :key name of a hash key attribute
212
+ # @option options [Symbol] :key_type type of a hash key attribute
203
213
  # @option options [Symbol] :inheritance_field name of an attribute used for STI
204
214
  # @option options [Symbol] :capacity_mode table billing mode - either +provisioned+ or +on_demand+
205
215
  # @option options [Integer] :write_capacity table write capacity units
@@ -210,11 +220,11 @@ module Dynamoid
210
220
  # @since 0.4.0
211
221
  def table(options)
212
222
  self.options = options
213
-
214
223
  # a default 'id' column is created when Dynamoid::Document is included
215
224
  unless attributes.key? hash_key
216
225
  remove_field :id
217
- field(hash_key)
226
+ key_type = options[:key_type] || :string
227
+ field(hash_key, key_type)
218
228
  end
219
229
 
220
230
  # The created_at/updated_at fields are declared in the `included` callback first.
@@ -291,7 +301,7 @@ module Dynamoid
291
301
  old_value = read_attribute(name)
292
302
 
293
303
  unless attribute_is_present_on_model?(name)
294
- raise Dynamoid::Errors::UnknownAttribute, "Attribute #{name} is not part of the model"
304
+ raise Dynamoid::Errors::UnknownAttribute.new(self.class, name)
295
305
  end
296
306
 
297
307
  if association = @associations[name]
@@ -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+
@@ -107,14 +107,28 @@ module Dynamoid
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
@@ -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
@@ -6,23 +6,25 @@ module Dynamoid
6
6
  module Persistence
7
7
  # @private
8
8
  class Inc
9
- def self.call(model_class, hash_key, range_key = nil, counters)
10
- 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
11
11
  end
12
12
 
13
13
  # rubocop:disable Style/OptionalArguments
14
- def initialize(model_class, hash_key, range_key = nil, counters)
14
+ def initialize(model_class, partition_key, sort_key = nil, counters)
15
15
  @model_class = model_class
16
- @hash_key = hash_key
17
- @range_key = range_key
16
+ @partition_key = partition_key
17
+ @sort_key = sort_key
18
18
  @counters = counters
19
19
  end
20
20
  # rubocop:enable Style/OptionalArguments
21
21
 
22
22
  def call
23
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)
24
26
 
25
- Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
27
+ Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options) do |t|
26
28
  item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
27
29
 
28
30
  @counters.each do |name, value|
@@ -37,19 +39,28 @@ module Dynamoid
37
39
  end
38
40
  end
39
41
  end
42
+ rescue Dynamoid::Errors::ConditionalCheckFailedException # rubocop:disable Lint/SuppressedException
40
43
  end
41
44
 
42
45
  private
43
46
 
44
- 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
+
45
57
  if @model_class.range_key
46
- range_key_options = @model_class.attributes[@model_class.range_key]
47
- value_casted = TypeCasting.cast_field(@range_key, range_key_options)
48
- value_dumped = Dumping.dump_field(value_casted, range_key_options)
49
- { range_key: value_dumped }
50
- else
51
- {}
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
52
61
  end
62
+
63
+ options
53
64
  end
54
65
 
55
66
  def timestamp_attributes_to_touch(touch)
@@ -60,6 +71,12 @@ module Dynamoid
60
71
  names += Array.wrap(touch) if touch != true
61
72
  names
62
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
63
80
  end
64
81
  end
65
82
  end