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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -6
- data/README.md +202 -25
- data/dynamoid.gemspec +5 -6
- data/lib/dynamoid/adapter.rb +19 -13
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
- data/lib/dynamoid/associations/belongs_to.rb +6 -6
- data/lib/dynamoid/associations.rb +1 -1
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config/options.rb +12 -12
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +149 -142
- data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
- data/lib/dynamoid/criteria/where_conditions.rb +36 -0
- data/lib/dynamoid/dirty.rb +87 -12
- data/lib/dynamoid/document.rb +1 -1
- data/lib/dynamoid/dumping.rb +38 -16
- data/lib/dynamoid/errors.rb +14 -2
- data/lib/dynamoid/fields/declare.rb +6 -6
- data/lib/dynamoid/fields.rb +6 -8
- data/lib/dynamoid/finders.rb +23 -32
- data/lib/dynamoid/indexes.rb +6 -7
- data/lib/dynamoid/loadable.rb +3 -2
- data/lib/dynamoid/persistence/inc.rb +6 -7
- data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
- data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
- data/lib/dynamoid/persistence/save.rb +17 -18
- data/lib/dynamoid/persistence/update_fields.rb +7 -5
- data/lib/dynamoid/persistence/update_validations.rb +1 -1
- data/lib/dynamoid/persistence/upsert.rb +5 -4
- data/lib/dynamoid/persistence.rb +77 -21
- data/lib/dynamoid/transaction_write/base.rb +47 -0
- data/lib/dynamoid/transaction_write/create.rb +49 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
- data/lib/dynamoid/transaction_write/destroy.rb +79 -0
- data/lib/dynamoid/transaction_write/save.rb +164 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
- data/lib/dynamoid/transaction_write/upsert.rb +96 -0
- data/lib/dynamoid/transaction_write.rb +464 -0
- data/lib/dynamoid/type_casting.rb +18 -15
- data/lib/dynamoid/undumping.rb +14 -3
- data/lib/dynamoid/validations.rb +1 -1
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +7 -0
- metadata +30 -16
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e13af88750cfc2b8421710068215b18cf596f86b0c5caa207574535af04369cc
|
4
|
+
data.tar.gz: b0cc179fb6c547fd766c788300bfe8fc38a9e10117895c0b39b665fcc609e448
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
-
|
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.
|
136
|
-
5.2, 6.0, 6.1 and 7.
|
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
|
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(
|
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(
|
930
|
-
User.where(
|
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' },
|
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
|
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.
|
55
|
-
spec.
|
56
|
-
spec.
|
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
|
data/lib/dynamoid/adapter.rb
CHANGED
@@ -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 =
|
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 [
|
176
|
-
# @
|
177
|
-
# @
|
178
|
-
# @option
|
179
|
-
# @option
|
180
|
-
# @option
|
181
|
-
# @option
|
182
|
-
#
|
183
|
-
# @
|
184
|
-
#
|
185
|
-
|
186
|
-
|
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 =>
|
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
|
-
|
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
|