dynamoid 3.10.0 → 3.11.0

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