dynamoid 3.10.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +182 -2
  4. data/dynamoid.gemspec +4 -4
  5. data/lib/dynamoid/adapter.rb +1 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +9 -5
  13. data/lib/dynamoid/components.rb +1 -0
  14. data/lib/dynamoid/config.rb +2 -0
  15. data/lib/dynamoid/criteria/chain.rb +63 -18
  16. data/lib/dynamoid/criteria/where_conditions.rb +13 -6
  17. data/lib/dynamoid/dirty.rb +86 -11
  18. data/lib/dynamoid/dumping.rb +36 -14
  19. data/lib/dynamoid/errors.rb +14 -2
  20. data/lib/dynamoid/finders.rb +6 -6
  21. data/lib/dynamoid/loadable.rb +1 -0
  22. data/lib/dynamoid/persistence/inc.rb +6 -7
  23. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  24. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  25. data/lib/dynamoid/persistence/save.rb +5 -2
  26. data/lib/dynamoid/persistence/update_fields.rb +5 -3
  27. data/lib/dynamoid/persistence/upsert.rb +5 -4
  28. data/lib/dynamoid/persistence.rb +38 -17
  29. data/lib/dynamoid/transaction_write/base.rb +47 -0
  30. data/lib/dynamoid/transaction_write/create.rb +49 -0
  31. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  32. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  33. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  34. data/lib/dynamoid/transaction_write/save.rb +164 -0
  35. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  36. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  37. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  38. data/lib/dynamoid/transaction_write.rb +464 -0
  39. data/lib/dynamoid/type_casting.rb +3 -1
  40. data/lib/dynamoid/undumping.rb +13 -2
  41. data/lib/dynamoid/validations.rb +1 -1
  42. data/lib/dynamoid/version.rb +1 -1
  43. data/lib/dynamoid.rb +7 -0
  44. metadata +18 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2cc1feec6853b756fff467fa687cf2eaa3a2dc5f0a88dac948e5a92a4aa1073
4
- data.tar.gz: 7165e081fa9979c45e7402265138e62cb6ee3c1f6f594ab6949c1f984621bc89
3
+ metadata.gz: e13af88750cfc2b8421710068215b18cf596f86b0c5caa207574535af04369cc
4
+ data.tar.gz: b0cc179fb6c547fd766c788300bfe8fc38a9e10117895c0b39b665fcc609e448
5
5
  SHA512:
6
- metadata.gz: e18700ff74b9466e1ec166706446a1dca5248c6b8c33365e469c74e9a67b92f4afc9cacb0a06c2698d5165f4b68a93cdd214b83f5e3b749b92bcaff06fbc7628
7
- data.tar.gz: 9b18fb2522f90eb3cf9b0b6014ca24beb08bcdebf403c57487222c46ec71d306f4e85e686aa89607c77ad7d994359a934537e8d22202b145bd478c0862bf497b
6
+ metadata.gz: 3b363e83838f36600b1af31c59406ac4fccad981e24af8762d5ae73fd1b648a2d03754483be89ed648f7ca982dc1640901af242c0b626ef9fb34b3002438edb4
7
+ data.tar.gz: a1bc08e5ab754f0a2ee383d2ac335396e7e058ad3b91df2d7c532d346da79daa0d239053ab856a8dfd0a2bdfb213f70a654cecdd3b42901c0261e61bcaca9ad5
data/CHANGELOG.md CHANGED
@@ -11,6 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ### Changed
12
12
  ### Removed
13
13
 
14
+ ## 3.11.0
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
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)
27
+
14
28
  ## 3.10.0
15
29
  ### Fixed
16
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`
data/README.md CHANGED
@@ -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
@@ -934,7 +973,6 @@ validation and callbacks.
934
973
  Address.find(id).update_attributes(city: 'Chicago')
935
974
  Address.find(id).update_attribute(:city, 'Chicago')
936
975
  Address.update(id, city: 'Chicago')
937
- Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
938
976
  ```
939
977
 
940
978
  There are also some low level methods `#update`, `.update_fields` and
@@ -1047,6 +1085,143 @@ resolving the fields with a second query against the table since a query
1047
1085
  against GSI then a query on base table is still likely faster than scan
1048
1086
  on the base table*
1049
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
+
1050
1225
  ### PartiQL
1051
1226
 
1052
1227
  To run PartiQL statements `Dynamoid.adapter.execute` method should be
@@ -1130,9 +1305,13 @@ Listed below are all configuration options.
1130
1305
  fields in ISO 8601 string format. Default is `false`
1131
1306
  * `store_date_as_string` - if `true` then Dynamoid stores :date fields
1132
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`
1133
1309
  * `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
1134
1310
  as native DynamoDB boolean values. Otherwise boolean fields are stored
1135
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`
1136
1315
  * `backoff` - is a hash: key is a backoff strategy (symbol), value is
1137
1316
  parameters for the strategy. Is used in batch operations. Default id
1138
1317
  `nil`
@@ -1350,6 +1529,7 @@ just as accessible to the Ruby world as MongoDB.
1350
1529
  Also, without contributors the project wouldn't be nearly as awesome. So
1351
1530
  many thanks to:
1352
1531
 
1532
+ * [Chris Hobbs](https://github.com/ckhsponge)
1353
1533
  * [Logan Bowers](https://github.com/loganb)
1354
1534
  * [Lane LaRue](https://github.com/luxx)
1355
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,9 +51,9 @@ Gem::Specification.new do |spec|
51
51
  spec.metadata['wiki_uri'] = 'https://github.com/Dynamoid/dynamoid/wiki'
52
52
  spec.metadata['rubygems_mfa_required'] = 'true'
53
53
 
54
- spec.add_runtime_dependency 'activemodel', '>=4'
55
- spec.add_runtime_dependency 'aws-sdk-dynamodb', '~> 1.0'
56
- spec.add_runtime_dependency 'concurrent-ruby', '>= 1.0'
54
+ spec.add_dependency 'activemodel', '>=4'
55
+ spec.add_dependency 'aws-sdk-dynamodb', '~> 1.0'
56
+ spec.add_dependency 'concurrent-ruby', '>= 1.0'
57
57
 
58
58
  spec.add_development_dependency 'appraisal'
59
59
  spec.add_development_dependency 'bundler'
@@ -118,7 +118,7 @@ module Dynamoid
118
118
  # @param [Hash] query a hash of attributes: matching records will be returned by the scan
119
119
  #
120
120
  # @since 0.2.0
121
- def scan(table, query = {}, opts = {})
121
+ def scan(table, query = [], opts = {})
122
122
  benchmark('Scan', table, query) { adapter.scan(table, query, opts) }
123
123
  end
124
124
 
@@ -20,48 +20,83 @@ module Dynamoid
20
20
  private
21
21
 
22
22
  def build
23
- clauses = @conditions.map do |name, attribute_conditions|
23
+ clauses = []
24
+
25
+ @conditions.each do |conditions|
26
+ if conditions.is_a? Hash
27
+ clauses << build_for_hash(conditions) unless conditions.empty?
28
+ elsif conditions.is_a? Array
29
+ query, placeholders = conditions
30
+ clauses << build_for_string(query, placeholders)
31
+ else
32
+ raise ArgumentError, "expected Hash or Array but actual value is #{conditions}"
33
+ end
34
+ end
35
+
36
+ @expression = clauses.join(' AND ')
37
+ end
38
+
39
+ def build_for_hash(hash)
40
+ clauses = hash.map do |name, attribute_conditions|
24
41
  attribute_conditions.map do |operator, value|
25
- name_or_placeholder = name_or_placeholder_for(name)
42
+ # replace attribute names with placeholders unconditionally to support
43
+ # - special characters (e.g. '.', ':', and '#') and
44
+ # - leading '_'
45
+ # See
46
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
47
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
48
+ name_placeholder = name_placeholder_for(name)
26
49
 
27
50
  case operator
28
51
  when :eq
29
- "#{name_or_placeholder} = #{value_placeholder_for(value)}"
52
+ "#{name_placeholder} = #{value_placeholder_for(value)}"
30
53
  when :ne
31
- "#{name_or_placeholder} <> #{value_placeholder_for(value)}"
54
+ "#{name_placeholder} <> #{value_placeholder_for(value)}"
32
55
  when :gt
33
- "#{name_or_placeholder} > #{value_placeholder_for(value)}"
56
+ "#{name_placeholder} > #{value_placeholder_for(value)}"
34
57
  when :lt
35
- "#{name_or_placeholder} < #{value_placeholder_for(value)}"
58
+ "#{name_placeholder} < #{value_placeholder_for(value)}"
36
59
  when :gte
37
- "#{name_or_placeholder} >= #{value_placeholder_for(value)}"
60
+ "#{name_placeholder} >= #{value_placeholder_for(value)}"
38
61
  when :lte
39
- "#{name_or_placeholder} <= #{value_placeholder_for(value)}"
62
+ "#{name_placeholder} <= #{value_placeholder_for(value)}"
40
63
  when :between
41
- "#{name_or_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
64
+ "#{name_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
42
65
  when :begins_with
43
- "begins_with (#{name_or_placeholder}, #{value_placeholder_for(value)})"
66
+ "begins_with (#{name_placeholder}, #{value_placeholder_for(value)})"
44
67
  when :in
45
68
  list = value.map(&method(:value_placeholder_for)).join(' , ')
46
- "#{name_or_placeholder} IN (#{list})"
69
+ "#{name_placeholder} IN (#{list})"
47
70
  when :contains
48
- "contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
71
+ "contains (#{name_placeholder}, #{value_placeholder_for(value)})"
49
72
  when :not_contains
50
- "NOT contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
73
+ "NOT contains (#{name_placeholder}, #{value_placeholder_for(value)})"
51
74
  when :null
52
- "attribute_not_exists (#{name_or_placeholder})"
75
+ "attribute_not_exists (#{name_placeholder})"
53
76
  when :not_null
54
- "attribute_exists (#{name_or_placeholder})"
77
+ "attribute_exists (#{name_placeholder})"
55
78
  end
56
79
  end
57
80
  end.flatten
58
81
 
59
- @expression = clauses.join(' AND ')
82
+ if clauses.empty?
83
+ nil
84
+ else
85
+ clauses.join(' AND ')
86
+ end
60
87
  end
61
88
 
62
- def name_or_placeholder_for(name)
63
- return name unless name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
89
+ def build_for_string(query, placeholders)
90
+ placeholders.each do |(k, v)|
91
+ k = k.to_s
92
+ k = ":#{k}" unless k.start_with?(':')
93
+ @value_placeholders[k] = v
94
+ end
95
+
96
+ "(#{query})"
97
+ end
64
98
 
99
+ def name_placeholder_for(name)
65
100
  placeholder = @name_placeholder_sequence.call
66
101
  @name_placeholders[placeholder] = name
67
102
  placeholder
@@ -97,10 +97,11 @@ module Dynamoid
97
97
 
98
98
  private
99
99
 
100
- # Keep in sync with AwsSdkV3.sanitize_item.
100
+ # It's a single low level component available in a public API (with
101
+ # Document#update/#update! methods). So duplicate sanitizing to some
102
+ # degree.
101
103
  #
102
- # The only difference is that to update item we need to track whether
103
- # attribute value is nil or not.
104
+ # Keep in sync with AwsSdkV3.sanitize_item.
104
105
  def sanitize_attributes(attributes)
105
106
  # rubocop:disable Lint/DuplicateBranch
106
107
  attributes.transform_values do |v|
@@ -108,7 +109,7 @@ module Dynamoid
108
109
  v.stringify_keys
109
110
  elsif v.is_a?(Set) && v.empty?
110
111
  nil
111
- elsif v.is_a?(String) && v.empty?
112
+ elsif v.is_a?(String) && v.empty? && Config.store_empty_string_as_nil
112
113
  nil
113
114
  else
114
115
  v
@@ -21,13 +21,15 @@ module Dynamoid
21
21
  return if @names.nil? || @names.empty?
22
22
 
23
23
  clauses = @names.map do |name|
24
- if name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
25
- placeholder = @name_placeholder_sequence.call
26
- @name_placeholders[placeholder] = name
27
- placeholder
28
- else
29
- name.to_s
30
- end
24
+ # replace attribute names with placeholders unconditionally to support
25
+ # - special characters (e.g. '.', ':', and '#') and
26
+ # - leading '_'
27
+ # See
28
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
29
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
30
+ placeholder = @name_placeholder_sequence.call
31
+ @name_placeholders[placeholder] = name
32
+ placeholder
31
33
  end
32
34
 
33
35
  @expression = clauses.join(' , ')
@@ -69,7 +69,7 @@ module Dynamoid
69
69
  limit = [record_limit, scan_limit, batch_size].compact.min
70
70
 
71
71
  # key condition expression
72
- convertor = FilterExpressionConvertor.new(@key_conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
72
+ convertor = FilterExpressionConvertor.new([@key_conditions], name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
73
73
  key_condition_expression = convertor.expression
74
74
  value_placeholders = convertor.value_placeholders
75
75
  name_placeholders = convertor.name_placeholders
@@ -13,7 +13,7 @@ module Dynamoid
13
13
  class Scan
14
14
  attr_reader :client, :table, :conditions, :options
15
15
 
16
- def initialize(client, table, conditions = {}, options = {})
16
+ def initialize(client, table, conditions = [], options = {})
17
17
  @client = client
18
18
  @table = table
19
19
  @conditions = conditions
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Prepare all the actions of the transaction for sending to the AWS SDK.
4
+ module Dynamoid
5
+ module AdapterPlugin
6
+ class AwsSdkV3
7
+ class Transact
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ # Perform all of the item actions in a single transaction.
15
+ #
16
+ # @param [Array] items of type Dynamoid::Transaction::Action or
17
+ # any other object whose to_h is a transact_item hash
18
+ #
19
+ def transact_write_items(items)
20
+ transact_items = items.map(&:to_h)
21
+ params = {
22
+ transact_items: transact_items,
23
+ return_consumed_capacity: 'TOTAL',
24
+ return_item_collection_metrics: 'SIZE'
25
+ }
26
+ client.transact_write_items(params) # returns this
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -8,6 +8,7 @@ require_relative 'aws_sdk_v3/batch_get_item'
8
8
  require_relative 'aws_sdk_v3/item_updater'
9
9
  require_relative 'aws_sdk_v3/table'
10
10
  require_relative 'aws_sdk_v3/until_past_table_status'
11
+ require_relative 'aws_sdk_v3/transact'
11
12
 
12
13
  module Dynamoid
13
14
  # @private
@@ -289,6 +290,10 @@ module Dynamoid
289
290
  raise Dynamoid::Errors::ConditionalCheckFailedException, e
290
291
  end
291
292
 
293
+ def transact_write_items(items)
294
+ Transact.new(client).transact_write_items(items)
295
+ end
296
+
292
297
  # Create a table on DynamoDB. This usually takes a long time to complete.
293
298
  #
294
299
  # @param [String] table_name the name of the table to create
@@ -512,7 +517,7 @@ module Dynamoid
512
517
  # @since 1.0.0
513
518
  #
514
519
  # @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
515
- def query(table_name, key_conditions, non_key_conditions = {}, options = {})
520
+ def query(table_name, key_conditions, non_key_conditions = [], options = {})
516
521
  Enumerator.new do |yielder|
517
522
  table = describe_table(table_name)
518
523
 
@@ -545,7 +550,7 @@ module Dynamoid
545
550
  # @since 1.0.0
546
551
  #
547
552
  # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
548
- def scan(table_name, conditions = {}, options = {})
553
+ def scan(table_name, conditions = [], options = {})
549
554
  Enumerator.new do |yielder|
550
555
  table = describe_table(table_name)
551
556
 
@@ -558,7 +563,7 @@ module Dynamoid
558
563
  end
559
564
  end
560
565
 
561
- def scan_count(table_name, conditions = {}, options = {})
566
+ def scan_count(table_name, conditions = [], options = {})
562
567
  table = describe_table(table_name)
563
568
  options[:select] = 'COUNT'
564
569
 
@@ -662,8 +667,7 @@ module Dynamoid
662
667
  store_attribute_with_nil_value = config_value.nil? ? false : !!config_value
663
668
 
664
669
  attributes.reject do |_, v|
665
- ((v.is_a?(Set) || v.is_a?(String)) && v.empty?) ||
666
- (!store_attribute_with_nil_value && v.nil?)
670
+ !store_attribute_with_nil_value && v.nil?
667
671
  end.transform_values do |v|
668
672
  v.is_a?(Hash) ? v.stringify_keys : v
669
673
  end
@@ -13,6 +13,7 @@ module Dynamoid
13
13
 
14
14
  define_model_callbacks :create, :save, :destroy, :update
15
15
  define_model_callbacks :initialize, :find, :touch, only: :after
16
+ define_model_callbacks :commit, :rollback, only: :after
16
17
 
17
18
  before_save :set_expires_field
18
19
  after_initialize :set_inheritance_field
@@ -48,7 +48,9 @@ module Dynamoid
48
48
  option :dynamodb_timezone, default: :utc # available values - :utc, :local, time zone name like "Hawaii"
49
49
  option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format
50
50
  option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
51
+ option :store_empty_string_as_nil, default: true # store attribute's empty String value as null
51
52
  option :store_boolean_as_native, default: true
53
+ option :store_binary_as_native, default: false
52
54
  option :backoff, default: nil # callable object to handle exceeding of table throughput limit
53
55
  option :backoff_strategies, default: {
54
56
  constant: BackoffStrategies::ConstantBackoff,