dynamoid 3.9.0 → 3.11.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/README.md +202 -25
  4. data/dynamoid.gemspec +5 -6
  5. data/lib/dynamoid/adapter.rb +19 -13
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
  14. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  15. data/lib/dynamoid/associations.rb +1 -1
  16. data/lib/dynamoid/components.rb +1 -0
  17. data/lib/dynamoid/config/options.rb +12 -12
  18. data/lib/dynamoid/config.rb +3 -0
  19. data/lib/dynamoid/criteria/chain.rb +149 -142
  20. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  21. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  22. data/lib/dynamoid/criteria/where_conditions.rb +36 -0
  23. data/lib/dynamoid/dirty.rb +87 -12
  24. data/lib/dynamoid/document.rb +1 -1
  25. data/lib/dynamoid/dumping.rb +38 -16
  26. data/lib/dynamoid/errors.rb +14 -2
  27. data/lib/dynamoid/fields/declare.rb +6 -6
  28. data/lib/dynamoid/fields.rb +6 -8
  29. data/lib/dynamoid/finders.rb +23 -32
  30. data/lib/dynamoid/indexes.rb +6 -7
  31. data/lib/dynamoid/loadable.rb +3 -2
  32. data/lib/dynamoid/persistence/inc.rb +6 -7
  33. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  34. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  35. data/lib/dynamoid/persistence/save.rb +17 -18
  36. data/lib/dynamoid/persistence/update_fields.rb +7 -5
  37. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  38. data/lib/dynamoid/persistence/upsert.rb +5 -4
  39. data/lib/dynamoid/persistence.rb +77 -21
  40. data/lib/dynamoid/transaction_write/base.rb +47 -0
  41. data/lib/dynamoid/transaction_write/create.rb +49 -0
  42. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  43. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  44. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  45. data/lib/dynamoid/transaction_write/save.rb +164 -0
  46. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  47. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  48. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  49. data/lib/dynamoid/transaction_write.rb +464 -0
  50. data/lib/dynamoid/type_casting.rb +18 -15
  51. data/lib/dynamoid/undumping.rb +14 -3
  52. data/lib/dynamoid/validations.rb +1 -1
  53. data/lib/dynamoid/version.rb +1 -1
  54. data/lib/dynamoid.rb +7 -0
  55. metadata +30 -16
  56. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  57. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aedb91a28972fd9357cbb260341e617c3d0d35ae05a620d5ac8312f5d3edbf96
4
- data.tar.gz: a026d8c3d1e53f4e6800dcffe2b8350da50f70d953a19ae86dfeff0f65555c8c
3
+ metadata.gz: e13af88750cfc2b8421710068215b18cf596f86b0c5caa207574535af04369cc
4
+ data.tar.gz: b0cc179fb6c547fd766c788300bfe8fc38a9e10117895c0b39b665fcc609e448
5
5
  SHA512:
6
- metadata.gz: dcb483ea21eaa16246d82603d18952a70bab15556b65ea94fad3ac9dbfe6358f00730e1825275d241a18675fdc0a2e4c3c4b0553acb41f9205e5fe350337f54c
7
- data.tar.gz: 4e4c96ed9b90ccab85f2f09ad848455affa681c93740dcf1f7b6283797f8e47bebd1c83a70a524eb6ea44906bdd6baec07549f67a9e6bd6561b857ea8997b692
6
+ metadata.gz: 3b363e83838f36600b1af31c59406ac4fccad981e24af8762d5ae73fd1b648a2d03754483be89ed648f7ca982dc1640901af242c0b626ef9fb34b3002438edb4
7
+ data.tar.gz: a1bc08e5ab754f0a2ee383d2ac335396e7e058ad3b91df2d7c532d346da79daa0d239053ab856a8dfd0a2bdfb213f70a654cecdd3b42901c0261e61bcaca9ad5
data/CHANGELOG.md CHANGED
@@ -7,14 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Fixed
10
-
11
10
  ### Added
11
+ ### Changed
12
+ ### Removed
12
13
 
14
+ ## 3.11.0
15
+
16
+ ### Fixed
17
+ * [#829](https://github.com/Dynamoid/dynamoid/pull/829) Fixed saving of in-place field changes
18
+ * [#812](https://github.com/Dynamoid/dynamoid/pull/812) Restored sanitizing of attribute names in `#where` conditions
19
+ * [#721](https://github.com/Dynamoid/dynamoid/pull/721) Fixed code examples in README.md (@ndjndj)
20
+ ### Added
21
+ * [#688](https://github.com/Dynamoid/dynamoid/pull/688) Transactional modifying was added utilizing `TransactWriteItems` operation (@ckhsponge)
22
+ * [#794](https://github.com/Dynamoid/dynamoid/pull/794) Added new config option `store_empty_string_as_nil`
23
+ * [#828](https://github.com/Dynamoid/dynamoid/pull/828) Added Ruby 3.4, Rails 8.0 and Rails 7.2 in CI
13
24
  ### Changed
25
+ * [#832](https://github.com/Dynamoid/dynamoid/pull/832) Support String condition expressions with `#where`
26
+ * [#822](https://github.com/Dynamoid/dynamoid/pull/822) Support binary type natively. Also added new config option `store_binary_as_native` (@dalibor)
14
27
 
15
- ### Removed
28
+ ## 3.10.0
29
+ ### Fixed
30
+ * [#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
+ * [#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)
32
+ ### Added
33
+ * [#656](https://github.com/Dynamoid/dynamoid/pull/656) Added a `create_table_on_save` configuration flag to create table on save (@imaximix)
34
+ * [#697](https://github.com/Dynamoid/dynamoid/pull/697) Ensure Ruby 3.3 and Rails 7.1 versions are supported and added them on CI
35
+ ### Changed
36
+ * [#655](https://github.com/Dynamoid/dynamoid/pull/655) Support multiple `where` in the same chain with multiple conditions for the same field
16
37
 
17
- ## 3.9.0
38
+ ## 3.9.0 / 2023-04-13
18
39
  ### Fixed
19
40
  * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Specs in JRuby; Support for JRuby 9.4.0.0 (@pboling)
20
41
  * [#624](https://github.com/Dynamoid/dynamoid/pull/624) Fixed `#increment!`/`#decrement!` methods and made them compatible with Rails counterparts
@@ -50,15 +71,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
71
  * `#touch`
51
72
  * `#increment!`
52
73
  * `#decrement!`
53
- * [#642](https://github.com/Dynamoid/dynamoid/pull/642) Run specs on CI agains Ruby 3.2
74
+ * [#642](https://github.com/Dynamoid/dynamoid/pull/642) Run specs on CI against Ruby 3.2
54
75
  * [#645](https://github.com/Dynamoid/dynamoid/pull/645) Added `after_find` callback
55
76
  ### Changed
56
77
  * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Switch to [`rubocop-lts`](https://rubocop-lts.gitlab.io/) (@pboling)
57
- ### Removed
58
78
  * [#633](https://github.com/Dynamoid/dynamoid/pull/633) Change `#inspect` method to return only attributes
59
79
  * [#623](https://github.com/Dynamoid/dynamoid/pull/623) Optimized performance of persisting to send only changed attributes in a request to DynamoDB
60
80
 
61
- ## 3.8.0
81
+ ## 3.8.0 / 2022-11-09
62
82
  ### Fixed
63
83
  * [#525](https://github.com/Dynamoid/dynamoid/pull/525) Don't mark an attribute as changed if new assigned value equals the old one (@a5-stable)
64
84
  * Minor changes in the documentation:
data/README.md CHANGED
@@ -67,10 +67,10 @@ For example, to configure AWS access:
67
67
  Create `config/initializers/aws.rb` as follows:
68
68
 
69
69
  ```ruby
70
- Aws.config.update({
71
- region: 'us-west-2',
70
+ Aws.config.update(
71
+ region: 'us-west-2',
72
72
  credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'),
73
- })
73
+ )
74
74
  ```
75
75
 
76
76
  Alternatively, if you don't want Aws connection settings to be
@@ -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 and 3.2, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
136
- 5.2, 6.0, 6.1 and 7.0.
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.
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
@@ -701,7 +719,7 @@ users = User.import([{ name: 'Josh' }, { name: 'Nick' }])
701
719
 
702
720
  ### Querying
703
721
 
704
- Querying can be done in one of three ways:
722
+ Querying can be done in one of the following ways:
705
723
 
706
724
  ```ruby
707
725
  Address.find(address.id) # Find directly by ID.
@@ -710,6 +728,27 @@ Address.where(city: 'Chicago').all # Find by any number of matching criteria.
710
728
  Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax.
711
729
  ```
712
730
 
731
+ There is also a way to `#where` with a condition expression:
732
+
733
+ ```ruby
734
+ Address.where('city = :c', c: 'Chicago')
735
+ ```
736
+
737
+ A condition expression may contain operators (e.g. `<`, `>=`, `<>`),
738
+ keywords (e.g. `AND`, `OR`, `BETWEEN`) and built-in functions (e.g.
739
+ `begins_with`, `contains`) (see (documentation
740
+ )[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html]
741
+ for full syntax description).
742
+
743
+ **Warning:** Values (specified for a String condition expression) are
744
+ sent as is so Dynamoid field types that aren't supported natively by
745
+ DynamoDB (e.g. `datetime` and `date`) require explicit casting.
746
+
747
+ **Warning:** String condition expressions will be used by DynamoDB only
748
+ at filtering, so conditions on key attributes should be specified as a
749
+ Hash to perform Query operation instead of Scan. Don't use key
750
+ attributes in `#where`'s String condition expressions.
751
+
713
752
  And you can also query on associations:
714
753
 
715
754
  ```ruby
@@ -723,18 +762,6 @@ join, but instead finds all the user's addresses and naively filters
723
762
  them in Ruby. For large associations this is a performance hit compared
724
763
  to relational database engines.
725
764
 
726
- **WARNING:** There is a limitation of conditions passed to `where`
727
- method. Only one condition for some particular field could be specified.
728
- The last one only will be applied and others will be ignored. E.g. in
729
- examples:
730
-
731
- ```ruby
732
- User.where('age.gt': 10, 'age.lt': 20)
733
- User.where(name: 'Mike').where('name.begins_with': 'Ed')
734
- ```
735
-
736
- the first one will be ignored and the last one will be used.
737
-
738
765
  **Warning:** There is a caveat with filtering documents by `nil` value
739
766
  attribute. By default Dynamoid ignores attributes with `nil` value and
740
767
  doesn't store them in a DynamoDB document. This behavior could be
@@ -752,7 +779,7 @@ If Dynamoid keeps `nil` value attributes `eq`/`ne` operators should be
752
779
  used instead:
753
780
 
754
781
  ```ruby
755
- Address.where('postcode': nil)
782
+ Address.where(postcode: nil)
756
783
  Address.where('postcode.ne': nil)
757
784
  ```
758
785
 
@@ -926,8 +953,8 @@ If you have a range index, Dynamoid provides a number of additional
926
953
  other convenience methods to make your life a little easier:
927
954
 
928
955
  ```ruby
929
- User.where("created_at.gt": DateTime.now - 1.day).all
930
- User.where("created_at.lt": DateTime.now - 1.day).all
956
+ User.where('created_at.gt': DateTime.now - 1.day).all
957
+ User.where('created_at.lt': DateTime.now - 1.day).all
931
958
  ```
932
959
 
933
960
  It also supports `gte` and `lte`. Turning those into symbols and
@@ -946,7 +973,6 @@ validation and callbacks.
946
973
  Address.find(id).update_attributes(city: 'Chicago')
947
974
  Address.find(id).update_attribute(:city, 'Chicago')
948
975
  Address.update(id, city: 'Chicago')
949
- Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
950
976
  ```
951
977
 
952
978
  There are also some low level methods `#update`, `.update_fields` and
@@ -975,7 +1001,7 @@ To idempotently create-but-not-update a record, apply the `unless_exists` condit
975
1001
  to its keys when you upsert.
976
1002
 
977
1003
  ```ruby
978
- Address.upsert(id, { city: 'Chicago' }, if: { unless_exists: [:id] })
1004
+ Address.upsert(id, { city: 'Chicago' }, { unless_exists: [:id] })
979
1005
  ```
980
1006
 
981
1007
  ### Deleting
@@ -1059,6 +1085,143 @@ resolving the fields with a second query against the table since a query
1059
1085
  against GSI then a query on base table is still likely faster than scan
1060
1086
  on the base table*
1061
1087
 
1088
+ ### Transactions in Dynamoid
1089
+
1090
+ > [!WARNING]
1091
+ > Please note that this API is experimental and can be changed in
1092
+ > future releases.
1093
+
1094
+ Multiple modifying actions can be grouped together and submitted as an
1095
+ all-or-nothing operation. Atomic modifying operations are supported in
1096
+ Dynamoid using transactions. If any action in the transaction fails they
1097
+ all fail.
1098
+
1099
+ The following actions are supported:
1100
+
1101
+ * `#create`/`#create!` - add a new model if it does not already exist
1102
+ * `#save`/`#save!` - create or update model
1103
+ * `#update_attributes`/`#update_attributes!` - modifies one or more attributes from an existig
1104
+ model
1105
+ * `#delete` - remove an model without callbacks nor validations
1106
+ * `#destroy`/`#destroy!` - remove an model
1107
+ * `#upsert` - add a new model or update an existing one, no callbacks
1108
+ * `#update_fields` - update a model without its instantiation
1109
+
1110
+ These methods are supposed to behave exactly like their
1111
+ non-transactional counterparts.
1112
+
1113
+
1114
+ #### Create models
1115
+
1116
+ Models can be created inside of a transaction. The partition and sort
1117
+ keys, if applicable, are used to determine uniqueness. Creating will
1118
+ fail with `Aws::DynamoDB::Errors::TransactionCanceledException` if a
1119
+ model already exists.
1120
+
1121
+ This example creates a user with a unique id and unique email address by
1122
+ creating 2 models. An additional model is upserted in the same
1123
+ transaction. Upsert will update `updated_at` but will not create
1124
+ `created_at`.
1125
+
1126
+ ```ruby
1127
+ user_id = SecureRandom.uuid
1128
+ email = 'bob@bob.bob'
1129
+
1130
+ Dynamoid::TransactionWrite.execute do |txn|
1131
+ txn.create(User, id: user_id)
1132
+ txn.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1133
+ txn.create(Address, id: 'A#2', street: '456')
1134
+ txn.upsert(Address, 'A#1', street: '123')
1135
+ end
1136
+ ```
1137
+
1138
+ #### Save models
1139
+
1140
+ Models can be saved in a transaction. New records are created otherwise
1141
+ the model is updated. Save, create, update, validate and destroy
1142
+ callbacks are called around the transaction as appropriate. Validation
1143
+ failures will throw `Dynamoid::Errors::DocumentNotValid`.
1144
+
1145
+ ```ruby
1146
+ user = User.find(1)
1147
+ article = Article.new(body: 'New article text', user_id: user.id)
1148
+
1149
+ Dynamoid::TransactionWrite.execute do |txn|
1150
+ txn.save(article)
1151
+
1152
+ user.last_article_id = article.id
1153
+ txn.save(user)
1154
+ end
1155
+ ```
1156
+
1157
+ #### Update models
1158
+
1159
+ A model can be updated by providing a model or primary key, and the fields to update.
1160
+
1161
+ ```ruby
1162
+ Dynamoid::TransactionWrite.execute do |txn|
1163
+ # change name and title for a user
1164
+ txn.update_attributes(user, name: 'bob', title: 'mister')
1165
+
1166
+ # sets the name and title for a user
1167
+ # The user is found by id (that equals 1)
1168
+ txn.update_fields(User, '1', name: 'bob', title: 'mister')
1169
+ end
1170
+ ```
1171
+
1172
+ #### Destroy or delete models
1173
+
1174
+ Models can be used or the model class and key can be specified.
1175
+ `#destroy` uses callbacks and validations. Use `#delete` to skip
1176
+ callbacks and validations.
1177
+
1178
+ ```ruby
1179
+ article = Article.find('1')
1180
+ tag = article.tag
1181
+
1182
+ Dynamoid::TransactionWrite.execute do |txn|
1183
+ txn.destroy(article)
1184
+ txn.delete(tag)
1185
+
1186
+ txn.delete(Tag, '2') # delete record with hash key '2' if it exists
1187
+ txn.delete(Tag, 'key#abcd', 'range#1') # when sort key is required
1188
+ end
1189
+ ```
1190
+
1191
+ #### Validation failures that don't raise
1192
+
1193
+ All of the transaction methods can be called without the `!` which
1194
+ results in `false` instead of a raised exception when validation fails.
1195
+ Ignoring validation failures can lead to confusion or bugs so always
1196
+ check return status when not using a method with `!`.
1197
+
1198
+ ```ruby
1199
+ user = User.find('1')
1200
+ user.red = true
1201
+
1202
+ Dynamoid::TransactionWrite.execute do |txn|
1203
+ if txn.save(user) # won't raise validation exception
1204
+ txn.update_fields(UserCount, user.id, count: 5)
1205
+ else
1206
+ puts 'ALERT: user not valid, skipping'
1207
+ end
1208
+ end
1209
+ ```
1210
+
1211
+ #### Incrementally building a transaction
1212
+
1213
+ Transactions can also be built without a block.
1214
+
1215
+ ```ruby
1216
+ transaction = Dynamoid::TransactionWrite.new
1217
+
1218
+ transaction.create(User, id: user_id)
1219
+ transaction.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1220
+ transaction.upsert(Address, 'A#1', street: '123')
1221
+
1222
+ transaction.commit # changes are persisted in this moment
1223
+ ```
1224
+
1062
1225
  ### PartiQL
1063
1226
 
1064
1227
  To run PartiQL statements `Dynamoid.adapter.execute` method should be
@@ -1142,14 +1305,18 @@ Listed below are all configuration options.
1142
1305
  fields in ISO 8601 string format. Default is `false`
1143
1306
  * `store_date_as_string` - if `true` then Dynamoid stores :date fields
1144
1307
  in ISO 8601 string format. Default is `false`
1308
+ * `store_empty_string_as_nil` - store attribute's empty String value as NULL. Default is `true`
1145
1309
  * `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
1146
1310
  as native DynamoDB boolean values. Otherwise boolean fields are stored
1147
- as string values `'t'` and `'f'`. Default is true
1311
+ as string values `'t'` and `'f'`. Default is `true`
1312
+ * `store_binary_as_native` - if `true` Dynamoid stores binary fields
1313
+ as native DynamoDB binary values. Otherwise binary fields are stored
1314
+ as Base64 encoded string values. Default is `false`
1148
1315
  * `backoff` - is a hash: key is a backoff strategy (symbol), value is
1149
1316
  parameters for the strategy. Is used in batch operations. Default id
1150
1317
  `nil`
1151
1318
  * `backoff_strategies`: is a hash and contains all available strategies.
1152
- Default is { constant: ..., exponential: ...}
1319
+ Default is `{ constant: ..., exponential: ...}`
1153
1320
  * `log_formatter`: overrides default AWS SDK formatter. There are
1154
1321
  several canned formatters: `Aws::Log::Formatter.default`,
1155
1322
  `Aws::Log::Formatter.colored` and `Aws::Log::Formatter.short`. Please
@@ -1167,6 +1334,9 @@ Listed below are all configuration options.
1167
1334
  * `http_read_timeout`:The number of seconds to wait for HTTP response
1168
1335
  data. Default option value is `nil`. If not specified effected value
1169
1336
  is `60`
1337
+ * `create_table_on_save`: if `true` then Dynamoid creates a
1338
+ corresponding table in DynamoDB at model persisting if the table
1339
+ doesn't exist yet. Default is `true`
1170
1340
 
1171
1341
 
1172
1342
  ## Concurrency
@@ -1305,6 +1475,12 @@ RSpec.configure do |config|
1305
1475
  end
1306
1476
  ```
1307
1477
 
1478
+ In addition, the first test for each model may fail if the relevant models are not included in `included_models`. This can be fixed by adding this line before the `DynamoidReset` module:
1479
+ ```ruby
1480
+ Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
1481
+ ```
1482
+ Note that this will require _all_ models in your models folder - you can also explicitly require only certain models if you would prefer to.
1483
+
1308
1484
  In Rails, you may also want to ensure you do not delete non-test data
1309
1485
  accidentally by adding the following to your test environment setup:
1310
1486
 
@@ -1353,6 +1529,7 @@ just as accessible to the Ruby world as MongoDB.
1353
1529
  Also, without contributors the project wouldn't be nearly as awesome. So
1354
1530
  many thanks to:
1355
1531
 
1532
+ * [Chris Hobbs](https://github.com/ckhsponge)
1356
1533
  * [Logan Bowers](https://github.com/loganb)
1357
1534
  * [Lane LaRue](https://github.com/luxx)
1358
1535
  * [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,16 +51,15 @@ 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'
60
60
  spec.add_development_dependency 'pry', '~> 0.14'
61
61
  spec.add_development_dependency 'rake', '~> 13.0'
62
+ spec.add_development_dependency 'rexml'
62
63
  spec.add_development_dependency 'rspec', '~> 3.12'
63
- # 'rubocop-lts' is for Ruby 2.3+, see https://rubocop-lts.gitlab.io/
64
- spec.add_development_dependency 'rubocop-lts', '~> 10.0'
65
64
  spec.add_development_dependency 'yard'
66
65
  end
@@ -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
 
@@ -171,19 +171,25 @@ module Dynamoid
171
171
  # only really useful for range queries, since it can only find by one hash key at once. Only provide
172
172
  # one range key to the hash.
173
173
  #
174
+ # Dynamoid.adapter.query('users', { id: [[:eq, '1']], age: [[:between, [10, 30]]] }, { batch_size: 1000 })
175
+ #
174
176
  # @param [String] table_name the name of the table
175
- # @param [Hash] opts the options to query the table with
176
- # @option opts [String] :hash_value the value of the hash key to find
177
- # @option opts [Range] :range_value find the range key within this range
178
- # @option opts [Number] :range_greater_than find range keys greater than this
179
- # @option opts [Number] :range_less_than find range keys less than this
180
- # @option opts [Number] :range_gte find range keys greater than or equal to this
181
- # @option opts [Number] :range_lte find range keys less than or equal to this
182
- #
183
- # @return [Array] an array of all matching items
184
- #
185
- def query(table_name, opts = {})
186
- adapter.query(table_name, opts)
177
+ # @param [Array[Array]] key_conditions conditions for the primary key attributes
178
+ # @param [Array[Array]] non_key_conditions (optional) conditions for non-primary key attributes
179
+ # @param [Hash] options (optional) the options to query the table with
180
+ # @option options [Boolean] :consistent_read You can set the ConsistentRead parameter to true and obtain a strongly consistent result
181
+ # @option options [Boolean] :scan_index_forward Specifies the order for index traversal: If true (default), the traversal is performed in ascending order; if false, the traversal is performed in descending order.
182
+ # @option options [Symbop] :select The attributes to be returned in the result (one of ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, ...)
183
+ # @option options [Symbol] :index_name The name of an index to query. This index can be any local secondary index or global secondary index on the table.
184
+ # @option options [Hash] :exclusive_start_key The primary key of the first item that this operation will evaluate.
185
+ # @option options [Integer] :batch_size The number of items to lazily load one by one
186
+ # @option options [Integer] :record_limit The maximum number of items to return (not necessarily the number of evaluated items)
187
+ # @option options [Integer] :scan_limit The maximum number of items to evaluate (not necessarily the number of matching items)
188
+ # @option options [Array[Symbol]] :project The attributes to retrieve from the table
189
+ #
190
+ # @return [Enumerable] matching items
191
+ def query(table_name, key_conditions, non_key_conditions = {}, options = {})
192
+ adapter.query(table_name, key_conditions, non_key_conditions, options)
187
193
  end
188
194
 
189
195
  def self.adapter_plugin_class
@@ -29,7 +29,7 @@ module Dynamoid
29
29
  gs_indexes = options[:global_secondary_indexes]
30
30
 
31
31
  key_schema = {
32
- hash_key_schema: { key => (options[:hash_key_type] || :string) },
32
+ hash_key_schema: { key => options[:hash_key_type] || :string },
33
33
  range_key_schema: options[:range_key]
34
34
  }
35
35
  attribute_definitions = build_all_attribute_definitions(
@@ -69,7 +69,7 @@ module Dynamoid
69
69
  end
70
70
  end
71
71
  resp = client.create_table(client_opts)
72
- options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
72
+ options[:sync] = true if (!options.key?(:sync) && ls_indexes.present?) || gs_indexes.present?
73
73
 
74
74
  if options[:sync]
75
75
  status = PARSE_TABLE_STATUS.call(resp, :table_description)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ # @private
5
+ module AdapterPlugin
6
+ class AwsSdkV3
7
+ class FilterExpressionConvertor
8
+ attr_reader :expression, :name_placeholders, :value_placeholders
9
+
10
+ def initialize(conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
11
+ @conditions = conditions
12
+ @name_placeholders = name_placeholders.dup
13
+ @value_placeholders = value_placeholders.dup
14
+ @name_placeholder_sequence = name_placeholder_sequence
15
+ @value_placeholder_sequence = value_placeholder_sequence
16
+
17
+ build
18
+ end
19
+
20
+ private
21
+
22
+ def build
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|
41
+ attribute_conditions.map do |operator, value|
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)
49
+
50
+ case operator
51
+ when :eq
52
+ "#{name_placeholder} = #{value_placeholder_for(value)}"
53
+ when :ne
54
+ "#{name_placeholder} <> #{value_placeholder_for(value)}"
55
+ when :gt
56
+ "#{name_placeholder} > #{value_placeholder_for(value)}"
57
+ when :lt
58
+ "#{name_placeholder} < #{value_placeholder_for(value)}"
59
+ when :gte
60
+ "#{name_placeholder} >= #{value_placeholder_for(value)}"
61
+ when :lte
62
+ "#{name_placeholder} <= #{value_placeholder_for(value)}"
63
+ when :between
64
+ "#{name_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
65
+ when :begins_with
66
+ "begins_with (#{name_placeholder}, #{value_placeholder_for(value)})"
67
+ when :in
68
+ list = value.map(&method(:value_placeholder_for)).join(' , ')
69
+ "#{name_placeholder} IN (#{list})"
70
+ when :contains
71
+ "contains (#{name_placeholder}, #{value_placeholder_for(value)})"
72
+ when :not_contains
73
+ "NOT contains (#{name_placeholder}, #{value_placeholder_for(value)})"
74
+ when :null
75
+ "attribute_not_exists (#{name_placeholder})"
76
+ when :not_null
77
+ "attribute_exists (#{name_placeholder})"
78
+ end
79
+ end
80
+ end.flatten
81
+
82
+ if clauses.empty?
83
+ nil
84
+ else
85
+ clauses.join(' AND ')
86
+ end
87
+ end
88
+
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
98
+
99
+ def name_placeholder_for(name)
100
+ placeholder = @name_placeholder_sequence.call
101
+ @name_placeholders[placeholder] = name
102
+ placeholder
103
+ end
104
+
105
+ def value_placeholder_for(value)
106
+ placeholder = @value_placeholder_sequence.call
107
+ @value_placeholders[placeholder] = value
108
+ placeholder
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -50,7 +50,19 @@ module Dynamoid
50
50
  # Replaces the values of one or more attributes
51
51
  #
52
52
  def set(values)
53
- @updates.merge!(sanitize_attributes(values))
53
+ values_sanitized = sanitize_attributes(values)
54
+
55
+ if Dynamoid.config.store_attribute_with_nil_value
56
+ @updates.merge!(values_sanitized)
57
+ else
58
+ # delete explicitly attributes if assigned nil value and configured
59
+ # to not store nil values
60
+ values_to_update = values_sanitized.reject { |_, v| v.nil? }
61
+ values_to_delete = values_sanitized.select { |_, v| v.nil? }
62
+
63
+ @updates.merge!(values_to_update)
64
+ @deletions.merge!(values_to_delete)
65
+ end
54
66
  end
55
67
 
56
68
  #
@@ -85,18 +97,25 @@ module Dynamoid
85
97
 
86
98
  private
87
99
 
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.
103
+ #
104
+ # Keep in sync with AwsSdkV3.sanitize_item.
88
105
  def sanitize_attributes(attributes)
106
+ # rubocop:disable Lint/DuplicateBranch
89
107
  attributes.transform_values do |v|
90
108
  if v.is_a?(Hash)
91
109
  v.stringify_keys
92
110
  elsif v.is_a?(Set) && v.empty?
93
111
  nil
94
- elsif v.is_a?(String) && v.empty?
112
+ elsif v.is_a?(String) && v.empty? && Config.store_empty_string_as_nil
95
113
  nil
96
114
  else
97
115
  v
98
116
  end
99
117
  end
118
+ # rubocop:enable Lint/DuplicateBranch
100
119
  end
101
120
  end
102
121
  end