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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2cc1feec6853b756fff467fa687cf2eaa3a2dc5f0a88dac948e5a92a4aa1073
4
- data.tar.gz: 7165e081fa9979c45e7402265138e62cb6ee3c1f6f594ab6949c1f984621bc89
3
+ metadata.gz: f3e330233bdd01c0bb962fb7253b48629fe9f0aeb1ecdcb6cfe7d3272ddddbe0
4
+ data.tar.gz: 7f6ee27e8d6dedb5fe21bce21a6c00397f42d85dfa40b8d30dae4e13a8294d15
5
5
  SHA512:
6
- metadata.gz: e18700ff74b9466e1ec166706446a1dca5248c6b8c33365e469c74e9a67b92f4afc9cacb0a06c2698d5165f4b68a93cdd214b83f5e3b749b92bcaff06fbc7628
7
- data.tar.gz: 9b18fb2522f90eb3cf9b0b6014ca24beb08bcdebf403c57487222c46ec71d306f4e85e686aa89607c77ad7d994359a934537e8d22202b145bd478c0862bf497b
6
+ metadata.gz: f84a868794dd6b812ccd6936eaa16c91a8d0f87b2ae959b89a8b01802537490168449bfa83f62d92f8643a6c47043cefeb9168da3301ca4f005400bc1086406f
7
+ data.tar.gz: 528587c73879f49d25e660974fa5832464eed5773da06535a76bfe3c9c96311a1877eb801e68362c94eadc479cf65a8df053a0f47574c78fbf5919859cb88e66
data/CHANGELOG.md CHANGED
@@ -11,7 +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.10.0
14
+ ## 3.12.0
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
38
+ ### Fixed
39
+ * [#829](https://github.com/Dynamoid/dynamoid/pull/829) Fixed saving of in-place field changes
40
+ * [#812](https://github.com/Dynamoid/dynamoid/pull/812) Restored sanitizing of attribute names in `#where` conditions
41
+ * [#721](https://github.com/Dynamoid/dynamoid/pull/721) Fixed code examples in README.md (@ndjndj)
42
+ ### Added
43
+ * [#688](https://github.com/Dynamoid/dynamoid/pull/688) Implemented write transactions (using DynamoDB's `TransactWriteItems` operation) (@ckhsponge)
44
+ * [#794](https://github.com/Dynamoid/dynamoid/pull/794) Added new config option `store_empty_string_as_nil`
45
+ * [#828](https://github.com/Dynamoid/dynamoid/pull/828) Added Ruby 3.4, Rails 8.0 and Rails 7.2 in CI
46
+ ### Changed
47
+ * [#832](https://github.com/Dynamoid/dynamoid/pull/832) Support String condition expressions with `#where`
48
+ * [#822](https://github.com/Dynamoid/dynamoid/pull/822) Support binary type natively. Also added new config option `store_binary_as_native` (@dalibor)
49
+
50
+ ## 3.10.0 / 2024-02-10
15
51
  ### Fixed
16
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`
17
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
 
@@ -342,6 +342,24 @@ class Document
342
342
  end
343
343
  ```
344
344
 
345
+ #### Note on binary type
346
+
347
+ By default binary fields are persisted as DynamoDB String value encoded
348
+ in the Base64 encoding. DynamoDB supports binary data natively. To use
349
+ it instead of String a `store_binary_as_native` field option should be
350
+ set:
351
+
352
+ ```ruby
353
+ class Document
354
+ include Dynamoid::Document
355
+
356
+ field :image, :binary, store_binary_as_native: true
357
+ end
358
+ ```
359
+
360
+ There is also a global config option `store_binary_as_native` that is
361
+ `false` by default as well.
362
+
345
363
  #### Magic Columns
346
364
 
347
365
  You get magic columns of `id` (`string`), `created_at` (`datetime`), and
@@ -453,6 +471,27 @@ method, which would return either `:string` or `:number`.
453
471
  DynamoDB may support some other attribute types that are not yet
454
472
  supported by Dynamoid.
455
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
+
456
495
  ### Sort key
457
496
 
458
497
  Along with partition key table may have a sort key. In order to declare
@@ -602,6 +641,7 @@ with `inheritance_field` table option:
602
641
  ```ruby
603
642
  class Car
604
643
  include Dynamoid::Document
644
+
605
645
  table inheritance_field: :my_new_type
606
646
 
607
647
  field :my_new_type
@@ -701,7 +741,7 @@ users = User.import([{ name: 'Josh' }, { name: 'Nick' }])
701
741
 
702
742
  ### Querying
703
743
 
704
- Querying can be done in one of three ways:
744
+ Querying can be done in one of the following ways:
705
745
 
706
746
  ```ruby
707
747
  Address.find(address.id) # Find directly by ID.
@@ -710,6 +750,27 @@ Address.where(city: 'Chicago').all # Find by any number of matching criteria.
710
750
  Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax.
711
751
  ```
712
752
 
753
+ There is also a way to `#where` with a condition expression:
754
+
755
+ ```ruby
756
+ Address.where('city = :c', c: 'Chicago')
757
+ ```
758
+
759
+ A condition expression may contain operators (e.g. `<`, `>=`, `<>`),
760
+ keywords (e.g. `AND`, `OR`, `BETWEEN`) and built-in functions (e.g.
761
+ `begins_with`, `contains`) (see (documentation
762
+ )[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html]
763
+ for full syntax description).
764
+
765
+ **Warning:** Values (specified for a String condition expression) are
766
+ sent as is so Dynamoid field types that aren't supported natively by
767
+ DynamoDB (e.g. `datetime` and `date`) require explicit casting.
768
+
769
+ **Warning:** String condition expressions will be used by DynamoDB only
770
+ at filtering, so conditions on key attributes should be specified as a
771
+ Hash to perform Query operation instead of Scan. Don't use key
772
+ attributes in `#where`'s String condition expressions.
773
+
713
774
  And you can also query on associations:
714
775
 
715
776
  ```ruby
@@ -875,6 +936,7 @@ It could be done with `project` method:
875
936
  ```ruby
876
937
  class User
877
938
  include Dynamoid::Document
939
+
878
940
  field :name
879
941
  end
880
942
 
@@ -934,7 +996,6 @@ validation and callbacks.
934
996
  Address.find(id).update_attributes(city: 'Chicago')
935
997
  Address.find(id).update_attribute(:city, 'Chicago')
936
998
  Address.update(id, city: 'Chicago')
937
- Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
938
999
  ```
939
1000
 
940
1001
  There are also some low level methods `#update`, `.update_fields` and
@@ -1047,6 +1108,198 @@ resolving the fields with a second query against the table since a query
1047
1108
  against GSI then a query on base table is still likely faster than scan
1048
1109
  on the base table*
1049
1110
 
1111
+ ### Transactions in Dynamoid
1112
+
1113
+ > [!WARNING]
1114
+ > Please note that this API is experimental and can be changed in
1115
+ > future releases.
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
+
1125
+ Multiple modifying actions can be grouped together and submitted as an
1126
+ all-or-nothing operation. Atomic modifying operations are supported in
1127
+ Dynamoid using transactions. If any action in the transaction fails they
1128
+ all fail.
1129
+
1130
+ The following actions are supported:
1131
+
1132
+ * `#create`/`#create!` - add a new model if it does not already exist
1133
+ * `#save`/`#save!` - create or update model
1134
+ * `#update_attributes`/`#update_attributes!` - modifies one or more attributes from an existig
1135
+ model
1136
+ * `#delete` - remove an model without callbacks nor validations
1137
+ * `#destroy`/`#destroy!` - remove an model
1138
+ * `#upsert` - add a new model or update an existing one, no callbacks
1139
+ * `#update_fields` - update a model without its instantiation
1140
+
1141
+ These methods are supposed to behave exactly like their
1142
+ non-transactional counterparts.
1143
+
1144
+ ##### Create models
1145
+
1146
+ Models can be created inside of a transaction. The partition and sort
1147
+ keys, if applicable, are used to determine uniqueness. Creating will
1148
+ fail with `Aws::DynamoDB::Errors::TransactionCanceledException` if a
1149
+ model already exists.
1150
+
1151
+ This example creates a user with a unique id and unique email address by
1152
+ creating 2 models. An additional model is upserted in the same
1153
+ transaction. Upsert will update `updated_at` but will not create
1154
+ `created_at`.
1155
+
1156
+ ```ruby
1157
+ user_id = SecureRandom.uuid
1158
+ email = 'bob@bob.bob'
1159
+
1160
+ Dynamoid::TransactionWrite.execute do |txn|
1161
+ txn.create(User, id: user_id)
1162
+ txn.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1163
+ txn.create(Address, id: 'A#2', street: '456')
1164
+ txn.upsert(Address, 'A#1', street: '123')
1165
+ end
1166
+ ```
1167
+
1168
+ ##### Save models
1169
+
1170
+ Models can be saved in a transaction. New records are created otherwise
1171
+ the model is updated. Save, create, update, validate and destroy
1172
+ callbacks are called around the transaction as appropriate. Validation
1173
+ failures will throw `Dynamoid::Errors::DocumentNotValid`.
1174
+
1175
+ ```ruby
1176
+ user = User.find(1)
1177
+ article = Article.new(body: 'New article text', user_id: user.id)
1178
+
1179
+ Dynamoid::TransactionWrite.execute do |txn|
1180
+ txn.save(article)
1181
+
1182
+ user.last_article_id = article.id
1183
+ txn.save(user)
1184
+ end
1185
+ ```
1186
+
1187
+ ##### Update models
1188
+
1189
+ A model can be updated by providing a model or primary key, and the fields to update.
1190
+
1191
+ ```ruby
1192
+ Dynamoid::TransactionWrite.execute do |txn|
1193
+ # change name and title for a user
1194
+ txn.update_attributes(user, name: 'bob', title: 'mister')
1195
+
1196
+ # sets the name and title for a user
1197
+ # The user is found by id (that equals 1)
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
1212
+ end
1213
+ ```
1214
+
1215
+ ##### Destroy or delete models
1216
+
1217
+ Models can be used or the model class and key can be specified.
1218
+ `#destroy` uses callbacks and validations. Use `#delete` to skip
1219
+ callbacks and validations.
1220
+
1221
+ ```ruby
1222
+ article = Article.find('1')
1223
+ tag = article.tag
1224
+
1225
+ Dynamoid::TransactionWrite.execute do |txn|
1226
+ txn.destroy(article)
1227
+ txn.delete(tag)
1228
+
1229
+ txn.delete(Tag, '2') # delete record with hash key '2' if it exists
1230
+ txn.delete(Tag, 'key#abcd', 'range#1') # when sort key is required
1231
+ end
1232
+ ```
1233
+
1234
+ ##### Validation failures that don't raise
1235
+
1236
+ All of the transaction methods can be called without the `!` which
1237
+ results in `false` instead of a raised exception when validation fails.
1238
+ Ignoring validation failures can lead to confusion or bugs so always
1239
+ check return status when not using a method with `!`.
1240
+
1241
+ ```ruby
1242
+ user = User.find('1')
1243
+ user.red = true
1244
+
1245
+ Dynamoid::TransactionWrite.execute do |txn|
1246
+ if txn.save(user) # won't raise validation exception
1247
+ txn.update_fields(UserCount, user.id, count: 5)
1248
+ else
1249
+ puts 'ALERT: user not valid, skipping'
1250
+ end
1251
+ end
1252
+ ```
1253
+
1254
+ ##### Incrementally building a transaction
1255
+
1256
+ Transactions can also be built without a block.
1257
+
1258
+ ```ruby
1259
+ transaction = Dynamoid::TransactionWrite.new
1260
+
1261
+ transaction.create(User, id: user_id)
1262
+ transaction.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1263
+ transaction.upsert(Address, 'A#1', street: '123')
1264
+
1265
+ transaction.commit # changes are persisted in this moment
1266
+ ```
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
+
1050
1303
  ### PartiQL
1051
1304
 
1052
1305
  To run PartiQL statements `Dynamoid.adapter.execute` method should be
@@ -1094,6 +1347,7 @@ Listed below are all configuration options.
1094
1347
  * `write_capacity` - is used at table or indices creation. Default is 20
1095
1348
  (units)
1096
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`
1097
1351
  * `endpoint` - if provided, it communicates with the DynamoDB listening
1098
1352
  at the endpoint. This is useful for testing with
1099
1353
  [DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html)
@@ -1130,9 +1384,13 @@ Listed below are all configuration options.
1130
1384
  fields in ISO 8601 string format. Default is `false`
1131
1385
  * `store_date_as_string` - if `true` then Dynamoid stores :date fields
1132
1386
  in ISO 8601 string format. Default is `false`
1387
+ * `store_empty_string_as_nil` - store attribute's empty String value as NULL. Default is `true`
1133
1388
  * `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
1134
1389
  as native DynamoDB boolean values. Otherwise boolean fields are stored
1135
1390
  as string values `'t'` and `'f'`. Default is `true`
1391
+ * `store_binary_as_native` - if `true` Dynamoid stores binary fields
1392
+ as native DynamoDB binary values. Otherwise binary fields are stored
1393
+ as Base64 encoded string values. Default is `false`
1136
1394
  * `backoff` - is a hash: key is a backoff strategy (symbol), value is
1137
1395
  parameters for the strategy. Is used in batch operations. Default id
1138
1396
  `nil`
@@ -1322,6 +1580,7 @@ order to troubleshoot and debug issues just set it:
1322
1580
  ```ruby
1323
1581
  class User
1324
1582
  include Dynamoid::Document
1583
+
1325
1584
  field name
1326
1585
  end
1327
1586
 
@@ -1350,6 +1609,7 @@ just as accessible to the Ruby world as MongoDB.
1350
1609
  Also, without contributors the project wouldn't be nearly as awesome. So
1351
1610
  many thanks to:
1352
1611
 
1612
+ * [Chris Hobbs](https://github.com/ckhsponge)
1353
1613
  * [Logan Bowers](https://github.com/loganb)
1354
1614
  * [Lane LaRue](https://github.com/luxx)
1355
1615
  * [Craig Heneveld](https://github.com/cheneveld)
data/dynamoid.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
31
31
  spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
32
- # Ignore not commited files
32
+ # Ignore not committed files
33
33
  spec.files = Dir[
34
34
  'CHANGELOG.md',
35
35
  'dynamoid.gemspec',
@@ -51,9 +51,9 @@ Gem::Specification.new do |spec|
51
51
  spec.metadata['wiki_uri'] = 'https://github.com/Dynamoid/dynamoid/wiki'
52
52
  spec.metadata['rubygems_mfa_required'] = 'true'
53
53
 
54
- spec.add_runtime_dependency 'activemodel', '>=4'
55
- spec.add_runtime_dependency 'aws-sdk-dynamodb', '~> 1.0'
56
- spec.add_runtime_dependency 'concurrent-ruby', '>= 1.0'
54
+ spec.add_dependency 'activemodel', '>=4'
55
+ spec.add_dependency 'aws-sdk-dynamodb', '~> 1.0'
56
+ spec.add_dependency 'concurrent-ruby', '>= 1.0'
57
57
 
58
58
  spec.add_development_dependency 'appraisal'
59
59
  spec.add_development_dependency 'bundler'
@@ -118,7 +118,7 @@ module Dynamoid
118
118
  # @param [Hash] query a hash of attributes: matching records will be returned by the scan
119
119
  #
120
120
  # @since 0.2.0
121
- def scan(table, query = {}, opts = {})
121
+ def scan(table, query = [], opts = {})
122
122
  benchmark('Scan', table, query) { adapter.scan(table, query, opts) }
123
123
  end
124
124
 
@@ -20,48 +20,83 @@ module Dynamoid
20
20
  private
21
21
 
22
22
  def build
23
- clauses = @conditions.map do |name, attribute_conditions|
23
+ clauses = []
24
+
25
+ @conditions.each do |conditions|
26
+ if conditions.is_a? Hash
27
+ clauses << build_for_hash(conditions) unless conditions.empty?
28
+ elsif conditions.is_a? Array
29
+ query, placeholders = conditions
30
+ clauses << build_for_string(query, placeholders)
31
+ else
32
+ raise ArgumentError, "expected Hash or Array but actual value is #{conditions}"
33
+ end
34
+ end
35
+
36
+ @expression = clauses.join(' AND ')
37
+ end
38
+
39
+ def build_for_hash(hash)
40
+ clauses = hash.map do |name, attribute_conditions|
24
41
  attribute_conditions.map do |operator, value|
25
- name_or_placeholder = name_or_placeholder_for(name)
42
+ # replace attribute names with placeholders unconditionally to support
43
+ # - special characters (e.g. '.', ':', and '#') and
44
+ # - leading '_'
45
+ # See
46
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
47
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
48
+ name_placeholder = name_placeholder_for(name)
26
49
 
27
50
  case operator
28
51
  when :eq
29
- "#{name_or_placeholder} = #{value_placeholder_for(value)}"
52
+ "#{name_placeholder} = #{value_placeholder_for(value)}"
30
53
  when :ne
31
- "#{name_or_placeholder} <> #{value_placeholder_for(value)}"
54
+ "#{name_placeholder} <> #{value_placeholder_for(value)}"
32
55
  when :gt
33
- "#{name_or_placeholder} > #{value_placeholder_for(value)}"
56
+ "#{name_placeholder} > #{value_placeholder_for(value)}"
34
57
  when :lt
35
- "#{name_or_placeholder} < #{value_placeholder_for(value)}"
58
+ "#{name_placeholder} < #{value_placeholder_for(value)}"
36
59
  when :gte
37
- "#{name_or_placeholder} >= #{value_placeholder_for(value)}"
60
+ "#{name_placeholder} >= #{value_placeholder_for(value)}"
38
61
  when :lte
39
- "#{name_or_placeholder} <= #{value_placeholder_for(value)}"
62
+ "#{name_placeholder} <= #{value_placeholder_for(value)}"
40
63
  when :between
41
- "#{name_or_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
64
+ "#{name_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
42
65
  when :begins_with
43
- "begins_with (#{name_or_placeholder}, #{value_placeholder_for(value)})"
66
+ "begins_with (#{name_placeholder}, #{value_placeholder_for(value)})"
44
67
  when :in
45
68
  list = value.map(&method(:value_placeholder_for)).join(' , ')
46
- "#{name_or_placeholder} IN (#{list})"
69
+ "#{name_placeholder} IN (#{list})"
47
70
  when :contains
48
- "contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
71
+ "contains (#{name_placeholder}, #{value_placeholder_for(value)})"
49
72
  when :not_contains
50
- "NOT contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
73
+ "NOT contains (#{name_placeholder}, #{value_placeholder_for(value)})"
51
74
  when :null
52
- "attribute_not_exists (#{name_or_placeholder})"
75
+ "attribute_not_exists (#{name_placeholder})"
53
76
  when :not_null
54
- "attribute_exists (#{name_or_placeholder})"
77
+ "attribute_exists (#{name_placeholder})"
55
78
  end
56
79
  end
57
80
  end.flatten
58
81
 
59
- @expression = clauses.join(' AND ')
82
+ if clauses.empty?
83
+ nil
84
+ else
85
+ clauses.join(' AND ')
86
+ end
60
87
  end
61
88
 
62
- def name_or_placeholder_for(name)
63
- return name unless name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
89
+ def build_for_string(query, placeholders)
90
+ placeholders.each do |(k, v)|
91
+ k = k.to_s
92
+ k = ":#{k}" unless k.start_with?(':')
93
+ @value_placeholders[k] = v
94
+ end
95
+
96
+ "(#{query})"
97
+ end
64
98
 
99
+ def name_placeholder_for(name)
65
100
  placeholder = @name_placeholder_sequence.call
66
101
  @name_placeholders[placeholder] = name
67
102
  placeholder
@@ -97,10 +97,11 @@ module Dynamoid
97
97
 
98
98
  private
99
99
 
100
- # Keep in sync with AwsSdkV3.sanitize_item.
100
+ # It's a single low level component available in a public API (with
101
+ # Document#update/#update! methods). So duplicate sanitizing to some
102
+ # degree.
101
103
  #
102
- # The only difference is that to update item we need to track whether
103
- # attribute value is nil or not.
104
+ # Keep in sync with AwsSdkV3.sanitize_item.
104
105
  def sanitize_attributes(attributes)
105
106
  # rubocop:disable Lint/DuplicateBranch
106
107
  attributes.transform_values do |v|
@@ -108,7 +109,7 @@ module Dynamoid
108
109
  v.stringify_keys
109
110
  elsif v.is_a?(Set) && v.empty?
110
111
  nil
111
- elsif v.is_a?(String) && v.empty?
112
+ elsif v.is_a?(String) && v.empty? && Config.store_empty_string_as_nil
112
113
  nil
113
114
  else
114
115
  v
@@ -21,13 +21,15 @@ module Dynamoid
21
21
  return if @names.nil? || @names.empty?
22
22
 
23
23
  clauses = @names.map do |name|
24
- if name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
25
- placeholder = @name_placeholder_sequence.call
26
- @name_placeholders[placeholder] = name
27
- placeholder
28
- else
29
- name.to_s
30
- end
24
+ # replace attribute names with placeholders unconditionally to support
25
+ # - special characters (e.g. '.', ':', and '#') and
26
+ # - leading '_'
27
+ # See
28
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
29
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
30
+ placeholder = @name_placeholder_sequence.call
31
+ @name_placeholders[placeholder] = name
32
+ placeholder
31
33
  end
32
34
 
33
35
  @expression = clauses.join(' , ')
@@ -69,7 +69,7 @@ module Dynamoid
69
69
  limit = [record_limit, scan_limit, batch_size].compact.min
70
70
 
71
71
  # key condition expression
72
- convertor = FilterExpressionConvertor.new(@key_conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
72
+ convertor = FilterExpressionConvertor.new([@key_conditions], name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
73
73
  key_condition_expression = convertor.expression
74
74
  value_placeholders = convertor.value_placeholders
75
75
  name_placeholders = convertor.name_placeholders
@@ -13,7 +13,7 @@ module Dynamoid
13
13
  class Scan
14
14
  attr_reader :client, :table, :conditions, :options
15
15
 
16
- def initialize(client, table, conditions = {}, options = {})
16
+ def initialize(client, table, conditions = [], options = {})
17
17
  @client = client
18
18
  @table = table
19
19
  @conditions = conditions
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Prepare all the actions of the transaction for sending to the AWS SDK.
4
+ module Dynamoid
5
+ module AdapterPlugin
6
+ class AwsSdkV3
7
+ class Transact
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ # Perform all of the item actions in a single transaction.
15
+ #
16
+ # @param [Array] items of type Dynamoid::Transaction::Action or
17
+ # any other object whose to_h is a transact_item hash
18
+ #
19
+ def transact_write_items(items)
20
+ transact_items = items.map(&:to_h)
21
+ params = {
22
+ transact_items: transact_items,
23
+ return_consumed_capacity: 'TOTAL',
24
+ return_item_collection_metrics: 'SIZE'
25
+ }
26
+ client.transact_write_items(params) # returns this
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end