dynamoid 3.9.0 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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