dynamoid 3.10.0 → 3.12.1
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 +37 -1
- data/README.md +268 -8
- data/dynamoid.gemspec +4 -4
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +74 -21
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +97 -11
- data/lib/dynamoid/dumping.rb +39 -17
- data/lib/dynamoid/errors.rb +30 -3
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +44 -23
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +35 -19
- 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 +29 -14
- data/lib/dynamoid/persistence/update_fields.rb +23 -8
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +22 -8
- data/lib/dynamoid/persistence.rb +184 -28
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- 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 +65 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
- data/lib/dynamoid/transaction_write/destroy.rb +84 -0
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +169 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
- data/lib/dynamoid/transaction_write/upsert.rb +106 -0
- data/lib/dynamoid/transaction_write.rb +673 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +8 -5
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +8 -0
- metadata +21 -5
@@ -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,18 @@ 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
|
+
|
297
|
+
def transact_read_items(items)
|
298
|
+
request = {
|
299
|
+
transact_items: items,
|
300
|
+
return_consumed_capacity: 'TOTAL',
|
301
|
+
}
|
302
|
+
client.transact_get_items(request)
|
303
|
+
end
|
304
|
+
|
292
305
|
# Create a table on DynamoDB. This usually takes a long time to complete.
|
293
306
|
#
|
294
307
|
# @param [String] table_name the name of the table to create
|
@@ -512,7 +525,7 @@ module Dynamoid
|
|
512
525
|
# @since 1.0.0
|
513
526
|
#
|
514
527
|
# @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 =
|
528
|
+
def query(table_name, key_conditions, non_key_conditions = [], options = {})
|
516
529
|
Enumerator.new do |yielder|
|
517
530
|
table = describe_table(table_name)
|
518
531
|
|
@@ -545,7 +558,7 @@ module Dynamoid
|
|
545
558
|
# @since 1.0.0
|
546
559
|
#
|
547
560
|
# @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 =
|
561
|
+
def scan(table_name, conditions = [], options = {})
|
549
562
|
Enumerator.new do |yielder|
|
550
563
|
table = describe_table(table_name)
|
551
564
|
|
@@ -558,7 +571,7 @@ module Dynamoid
|
|
558
571
|
end
|
559
572
|
end
|
560
573
|
|
561
|
-
def scan_count(table_name, conditions =
|
574
|
+
def scan_count(table_name, conditions = [], options = {})
|
562
575
|
table = describe_table(table_name)
|
563
576
|
options[:select] = 'COUNT'
|
564
577
|
|
@@ -662,8 +675,7 @@ module Dynamoid
|
|
662
675
|
store_attribute_with_nil_value = config_value.nil? ? false : !!config_value
|
663
676
|
|
664
677
|
attributes.reject do |_, v|
|
665
|
-
|
666
|
-
(!store_attribute_with_nil_value && v.nil?)
|
678
|
+
!store_attribute_with_nil_value && v.nil?
|
667
679
|
end.transform_values do |v|
|
668
680
|
v.is_a?(Hash) ? v.stringify_keys : v
|
669
681
|
end
|
data/lib/dynamoid/components.rb
CHANGED
@@ -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
|
data/lib/dynamoid/config.rb
CHANGED
@@ -36,6 +36,7 @@ module Dynamoid
|
|
36
36
|
option :read_capacity, default: 100
|
37
37
|
option :write_capacity, default: 20
|
38
38
|
option :warn_on_scan, default: true
|
39
|
+
option :error_on_scan, default: false
|
39
40
|
option :endpoint, default: nil
|
40
41
|
option :identity_map, default: false
|
41
42
|
option :timestamps, default: true
|
@@ -48,7 +49,9 @@ module Dynamoid
|
|
48
49
|
option :dynamodb_timezone, default: :utc # available values - :utc, :local, time zone name like "Hawaii"
|
49
50
|
option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format
|
50
51
|
option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
|
52
|
+
option :store_empty_string_as_nil, default: true # store attribute's empty String value as null
|
51
53
|
option :store_boolean_as_native, default: true
|
54
|
+
option :store_binary_as_native, default: false
|
52
55
|
option :backoff, default: nil # callable object to handle exceeding of table throughput limit
|
53
56
|
option :backoff_strategies, default: {
|
54
57
|
constant: BackoffStrategies::ConstantBackoff,
|
@@ -95,20 +95,27 @@ module Dynamoid
|
|
95
95
|
#
|
96
96
|
# Internally +where+ performs either +Scan+ or +Query+ operation.
|
97
97
|
#
|
98
|
+
# Conditions can be specified as an expression as well:
|
99
|
+
#
|
100
|
+
# Post.where('links_count = :v', v: 2)
|
101
|
+
#
|
102
|
+
# This way complex expressions can be constructed (e.g. with AND, OR, and NOT
|
103
|
+
# keyword):
|
104
|
+
#
|
105
|
+
# Address.where('city = :c AND (post_code = :pc1 OR post_code = :pc2)', city: 'A', pc1: '001', pc2: '002')
|
106
|
+
#
|
107
|
+
# See documentation for condition expression's syntax and examples:
|
108
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
109
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.FilterExpression.html
|
110
|
+
#
|
98
111
|
# @return [Dynamoid::Criteria::Chain]
|
99
112
|
# @since 0.2.0
|
100
|
-
def where(
|
101
|
-
|
102
|
-
|
103
|
-
|
113
|
+
def where(conditions, placeholders = nil)
|
114
|
+
if conditions.is_a?(Hash)
|
115
|
+
where_with_hash(conditions)
|
116
|
+
else
|
117
|
+
where_with_string(conditions, placeholders)
|
104
118
|
end
|
105
|
-
|
106
|
-
@where_conditions.update(args.symbolize_keys)
|
107
|
-
|
108
|
-
# we should re-initialize keys detector every time we change @where_conditions
|
109
|
-
@key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
|
110
|
-
|
111
|
-
self
|
112
119
|
end
|
113
120
|
|
114
121
|
# Turns on strongly consistent reads.
|
@@ -500,6 +507,29 @@ module Dynamoid
|
|
500
507
|
|
501
508
|
private
|
502
509
|
|
510
|
+
def where_with_hash(conditions)
|
511
|
+
detector = NonexistentFieldsDetector.new(conditions, @source)
|
512
|
+
if detector.found?
|
513
|
+
Dynamoid.logger.warn(detector.warning_message)
|
514
|
+
end
|
515
|
+
|
516
|
+
@where_conditions.update_with_hash(conditions.symbolize_keys)
|
517
|
+
|
518
|
+
# we should re-initialize keys detector every time we change @where_conditions
|
519
|
+
@key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
|
520
|
+
|
521
|
+
self
|
522
|
+
end
|
523
|
+
|
524
|
+
def where_with_string(query, placeholders)
|
525
|
+
@where_conditions.update_with_string(query, placeholders)
|
526
|
+
|
527
|
+
# we should re-initialize keys detector every time we change @where_conditions
|
528
|
+
@key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
|
529
|
+
|
530
|
+
self
|
531
|
+
end
|
532
|
+
|
503
533
|
# The actual records referenced by the association.
|
504
534
|
#
|
505
535
|
# @return [Enumerator] an iterator of the found records.
|
@@ -533,7 +563,7 @@ module Dynamoid
|
|
533
563
|
if @key_fields_detector.key_present?
|
534
564
|
raw_pages_via_query
|
535
565
|
else
|
536
|
-
|
566
|
+
validate_scan_conditions
|
537
567
|
raw_pages_via_scan
|
538
568
|
end
|
539
569
|
end
|
@@ -568,6 +598,12 @@ module Dynamoid
|
|
568
598
|
end
|
569
599
|
end
|
570
600
|
|
601
|
+
def validate_scan_conditions
|
602
|
+
raise Dynamoid::Errors::ScanProhibited if Dynamoid::Config.error_on_scan && !@where_conditions.empty?
|
603
|
+
|
604
|
+
issue_scan_warning if Dynamoid::Config.warn_on_scan && !@where_conditions.empty?
|
605
|
+
end
|
606
|
+
|
571
607
|
def issue_scan_warning
|
572
608
|
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
573
609
|
Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.underscore}.rb:"
|
@@ -635,12 +671,12 @@ module Dynamoid
|
|
635
671
|
end
|
636
672
|
|
637
673
|
def query_non_key_conditions
|
638
|
-
|
674
|
+
hash_conditions = {}
|
639
675
|
|
640
676
|
# Honor STI and :type field if it presents
|
641
677
|
if @source.attributes.key?(@source.inheritance_field) &&
|
642
678
|
@key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
|
643
|
-
@where_conditions.
|
679
|
+
@where_conditions.update_with_hash(sti_condition)
|
644
680
|
end
|
645
681
|
|
646
682
|
# TODO: Separate key conditions and non-key conditions properly:
|
@@ -650,22 +686,30 @@ module Dynamoid
|
|
650
686
|
.reject { |k, _| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }
|
651
687
|
keys.each do |key|
|
652
688
|
name, condition = field_condition(key, @where_conditions[key])
|
653
|
-
|
654
|
-
|
689
|
+
hash_conditions[name] ||= []
|
690
|
+
hash_conditions[name] << condition
|
655
691
|
end
|
656
692
|
|
657
|
-
|
693
|
+
string_conditions = []
|
694
|
+
@where_conditions.string_conditions.each do |query, placeholders|
|
695
|
+
placeholders ||= {}
|
696
|
+
string_conditions << [query, placeholders]
|
697
|
+
end
|
698
|
+
|
699
|
+
[hash_conditions] + string_conditions
|
658
700
|
end
|
659
701
|
|
660
702
|
# TODO: casting should be operator aware
|
661
703
|
# e.g. for NULL operator value should be boolean
|
662
704
|
# and isn't related to an attribute own type
|
663
705
|
def type_cast_condition_parameter(key, value)
|
664
|
-
|
706
|
+
field_type = source.attributes[key.to_sym][:type]
|
707
|
+
|
708
|
+
return value if %i[array set].include?(field_type)
|
665
709
|
|
666
710
|
if [true, false].include?(value) # Support argument for null/not_null operators
|
667
711
|
value
|
668
|
-
elsif !value.respond_to?(:to_ary)
|
712
|
+
elsif !value.respond_to?(:to_ary) || field_type == :serialized
|
669
713
|
options = source.attributes[key.to_sym]
|
670
714
|
value_casted = TypeCasting.cast_field(value, options)
|
671
715
|
Dumping.dump_field(value_casted, options)
|
@@ -721,16 +765,25 @@ module Dynamoid
|
|
721
765
|
def scan_conditions
|
722
766
|
# Honor STI and :type field if it presents
|
723
767
|
if sti_condition
|
724
|
-
@where_conditions.
|
768
|
+
@where_conditions.update_with_hash(sti_condition)
|
725
769
|
end
|
726
770
|
|
727
|
-
{}
|
771
|
+
hash_conditions = {}
|
772
|
+
hash_conditions.tap do |opts|
|
728
773
|
@where_conditions.keys.map(&:to_sym).each do |key|
|
729
774
|
name, condition = field_condition(key, @where_conditions[key])
|
730
775
|
opts[name] ||= []
|
731
776
|
opts[name] << condition
|
732
777
|
end
|
733
778
|
end
|
779
|
+
|
780
|
+
string_conditions = []
|
781
|
+
@where_conditions.string_conditions.each do |query, placeholders|
|
782
|
+
placeholders ||= {}
|
783
|
+
string_conditions << [query, placeholders]
|
784
|
+
end
|
785
|
+
|
786
|
+
[hash_conditions] + string_conditions
|
734
787
|
end
|
735
788
|
|
736
789
|
def scan_options
|
@@ -4,24 +4,31 @@ module Dynamoid
|
|
4
4
|
module Criteria
|
5
5
|
# @private
|
6
6
|
class WhereConditions
|
7
|
+
attr_reader :string_conditions
|
8
|
+
|
7
9
|
def initialize
|
8
|
-
@
|
10
|
+
@hash_conditions = []
|
11
|
+
@string_conditions = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_with_hash(hash)
|
15
|
+
@hash_conditions << hash.symbolize_keys
|
9
16
|
end
|
10
17
|
|
11
|
-
def
|
12
|
-
@
|
18
|
+
def update_with_string(query, placeholders)
|
19
|
+
@string_conditions << [query, placeholders]
|
13
20
|
end
|
14
21
|
|
15
22
|
def keys
|
16
|
-
@
|
23
|
+
@hash_conditions.flat_map(&:keys)
|
17
24
|
end
|
18
25
|
|
19
26
|
def empty?
|
20
|
-
@
|
27
|
+
@hash_conditions.empty? && @string_conditions.empty?
|
21
28
|
end
|
22
29
|
|
23
30
|
def [](key)
|
24
|
-
hash = @
|
31
|
+
hash = @hash_conditions.find { |h| h.key?(key) }
|
25
32
|
hash[key] if hash
|
26
33
|
end
|
27
34
|
end
|
data/lib/dynamoid/dirty.rb
CHANGED
@@ -36,8 +36,11 @@ module Dynamoid
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
-
def from_database(
|
40
|
-
super.tap
|
39
|
+
def from_database(attributes_from_database)
|
40
|
+
super.tap do |model|
|
41
|
+
model.clear_changes_information
|
42
|
+
model.assign_attributes_from_database(DeepDupper.dup_attributes(model.attributes, model.class))
|
43
|
+
end
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
@@ -108,7 +111,7 @@ module Dynamoid
|
|
108
111
|
#
|
109
112
|
# @return [ActiveSupport::HashWithIndifferentAccess]
|
110
113
|
def changes
|
111
|
-
ActiveSupport::HashWithIndifferentAccess[
|
114
|
+
ActiveSupport::HashWithIndifferentAccess[changed_attributes.map { |name, old_value| [name, [old_value, read_attribute(name)]] }]
|
112
115
|
end
|
113
116
|
|
114
117
|
# Returns a hash of attributes that were changed before the model was saved.
|
@@ -132,19 +135,21 @@ module Dynamoid
|
|
132
135
|
#
|
133
136
|
# @return [ActiveSupport::HashWithIndifferentAccess]
|
134
137
|
def changed_attributes
|
135
|
-
|
138
|
+
attributes_changed_by_setter.merge(attributes_changed_in_place)
|
136
139
|
end
|
137
140
|
|
138
141
|
# Clear all dirty data: current changes and previous changes.
|
139
142
|
def clear_changes_information
|
140
143
|
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
141
|
-
@
|
144
|
+
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
145
|
+
@attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
|
142
146
|
end
|
143
147
|
|
144
148
|
# Clears dirty data and moves +changes+ to +previous_changes+.
|
145
149
|
def changes_applied
|
146
150
|
@previously_changed = changes
|
147
|
-
@
|
151
|
+
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
152
|
+
@attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
|
148
153
|
end
|
149
154
|
|
150
155
|
# Remove changes information for the provided attributes.
|
@@ -152,6 +157,9 @@ module Dynamoid
|
|
152
157
|
# @param attributes [Array[String]] - a list of attributes to clear changes for
|
153
158
|
def clear_attribute_changes(names)
|
154
159
|
attributes_changed_by_setter.except!(*names)
|
160
|
+
|
161
|
+
slice = HashWithIndifferentAccess.new(@attributes).slice(*names)
|
162
|
+
attributes_from_database.merge!(DeepDupper.dup_attributes(slice, self.class))
|
155
163
|
end
|
156
164
|
|
157
165
|
# Handle <tt>*_changed?</tt> for +method_missing+.
|
@@ -220,12 +228,16 @@ module Dynamoid
|
|
220
228
|
previous_changes[name] if attribute_previously_changed?(name)
|
221
229
|
end
|
222
230
|
|
231
|
+
# @private
|
232
|
+
def assign_attributes_from_database(attributes_from_database)
|
233
|
+
@attributes_from_database = HashWithIndifferentAccess.new(attributes_from_database)
|
234
|
+
end
|
235
|
+
|
223
236
|
private
|
224
237
|
|
225
238
|
def changes_include?(name)
|
226
|
-
|
239
|
+
attribute_changed_by_setter?(name) || attribute_changed_in_place?(name)
|
227
240
|
end
|
228
|
-
alias attribute_changed_by_setter? changes_include?
|
229
241
|
|
230
242
|
# Handle <tt>*_change</tt> for +method_missing+.
|
231
243
|
def attribute_change(name)
|
@@ -259,13 +271,87 @@ module Dynamoid
|
|
259
271
|
previous_changes.include?(name)
|
260
272
|
end
|
261
273
|
|
262
|
-
|
263
|
-
|
264
|
-
|
274
|
+
def attributes_changed_by_setter
|
275
|
+
@attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
|
276
|
+
end
|
277
|
+
|
278
|
+
def attribute_changed_by_setter?(name)
|
279
|
+
attributes_changed_by_setter.include?(name)
|
280
|
+
end
|
281
|
+
|
282
|
+
def attributes_from_database
|
283
|
+
@attributes_from_database ||= ActiveSupport::HashWithIndifferentAccess.new
|
284
|
+
end
|
265
285
|
|
266
286
|
# Force an attribute to have a particular "before" value
|
267
287
|
def set_attribute_was(name, old_value)
|
268
288
|
attributes_changed_by_setter[name] = old_value
|
269
289
|
end
|
290
|
+
|
291
|
+
def attributes_changed_in_place
|
292
|
+
attributes_from_database.select do |name, _|
|
293
|
+
attribute_changed_in_place?(name)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def attribute_changed_in_place?(name)
|
298
|
+
return false if attribute_changed_by_setter?(name)
|
299
|
+
|
300
|
+
value_from_database = attributes_from_database[name]
|
301
|
+
return false if value_from_database.nil?
|
302
|
+
|
303
|
+
value = read_attribute(name)
|
304
|
+
type_options = self.class.attributes[name.to_sym]
|
305
|
+
|
306
|
+
unless type_options[:type].is_a?(Class) && !type_options[:comparable]
|
307
|
+
# common case
|
308
|
+
value != value_from_database
|
309
|
+
else
|
310
|
+
# objects of a custom type that does not implement its own `#==` method
|
311
|
+
# (that's declared by `comparable: false` or just not specifying the
|
312
|
+
# option `comparable`) are compared by comparing their dumps
|
313
|
+
dump = Dumping.dump_field(value, type_options)
|
314
|
+
dump_from_database = Dumping.dump_field(value_from_database, type_options)
|
315
|
+
dump != dump_from_database
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
module DeepDupper
|
320
|
+
def self.dup_attributes(attributes, klass)
|
321
|
+
attributes.map do |name, value|
|
322
|
+
type_options = klass.attributes[name.to_sym]
|
323
|
+
value_duplicate = dup_attribute(value, type_options)
|
324
|
+
[name, value_duplicate]
|
325
|
+
end.to_h
|
326
|
+
end
|
327
|
+
|
328
|
+
def self.dup_attribute(value, type_options)
|
329
|
+
type, of = type_options.values_at(:type, :of)
|
330
|
+
|
331
|
+
case value
|
332
|
+
when NilClass, TrueClass, FalseClass, Numeric, Symbol, IO
|
333
|
+
# Till Ruby 2.4 these immutable objects could not be duplicated.
|
334
|
+
# IO objects (used for the binary type) cannot be duplicated as well.
|
335
|
+
value
|
336
|
+
when Array
|
337
|
+
if of.is_a? Class
|
338
|
+
# custom type
|
339
|
+
value.map { |e| dup_attribute(e, type: of) }
|
340
|
+
else
|
341
|
+
value.deep_dup
|
342
|
+
end
|
343
|
+
when Set
|
344
|
+
Set.new(value.map { |e| dup_attribute(e, type: of) })
|
345
|
+
else
|
346
|
+
if type.is_a? Class
|
347
|
+
# custom type
|
348
|
+
dump = Dumping.dump_field(value, type_options)
|
349
|
+
Undumping.undump_field(dump.deep_dup, type_options)
|
350
|
+
else
|
351
|
+
value.deep_dup
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
270
356
|
end
|
271
357
|
end
|
data/lib/dynamoid/dumping.rb
CHANGED
@@ -70,7 +70,8 @@ module Dynamoid
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def invalid_value?(value)
|
73
|
-
(value.is_a?(Set)
|
73
|
+
(value.is_a?(Set) && value.empty?) ||
|
74
|
+
(value.is_a?(String) && value.empty? && Config.store_empty_string_as_nil)
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
@@ -86,6 +87,12 @@ module Dynamoid
|
|
86
87
|
|
87
88
|
# string -> string
|
88
89
|
class StringDumper < Base
|
90
|
+
def process(string)
|
91
|
+
return nil if string.nil?
|
92
|
+
return nil if string.empty? && Config.store_empty_string_as_nil
|
93
|
+
|
94
|
+
string
|
95
|
+
end
|
89
96
|
end
|
90
97
|
|
91
98
|
# integer -> number
|
@@ -101,6 +108,8 @@ module Dynamoid
|
|
101
108
|
ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
|
102
109
|
|
103
110
|
def process(set)
|
111
|
+
return nil if set.is_a?(Set) && set.empty?
|
112
|
+
|
104
113
|
if @options.key?(:of)
|
105
114
|
process_typed_collection(set)
|
106
115
|
else
|
@@ -112,13 +121,13 @@ module Dynamoid
|
|
112
121
|
|
113
122
|
def process_typed_collection(set)
|
114
123
|
if allowed_type?
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
if element_type == :string
|
119
|
-
result.reject!(&:empty?)
|
124
|
+
# StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
|
125
|
+
if element_type == :string && Config.store_empty_string_as_nil
|
126
|
+
set.reject! { |s| s && s.empty? }
|
120
127
|
end
|
121
128
|
|
129
|
+
dumper = Dumping.find_dumper(element_options)
|
130
|
+
result = set.map { |el| dumper.process(el) }
|
122
131
|
result.to_set
|
123
132
|
else
|
124
133
|
raise ArgumentError, "Set element type #{element_type} isn't supported"
|
@@ -164,14 +173,13 @@ module Dynamoid
|
|
164
173
|
|
165
174
|
def process_typed_collection(array)
|
166
175
|
if allowed_type?
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if element_type == :string
|
171
|
-
result.reject!(&:empty?)
|
176
|
+
# StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
|
177
|
+
if element_type == :string && Config.store_empty_string_as_nil
|
178
|
+
array.reject! { |s| s && s.empty? }
|
172
179
|
end
|
173
180
|
|
174
|
-
|
181
|
+
dumper = Dumping.find_dumper(element_options)
|
182
|
+
array.map { |el| dumper.process(el) }
|
175
183
|
else
|
176
184
|
raise ArgumentError, "Array element type #{element_type} isn't supported"
|
177
185
|
end
|
@@ -289,10 +297,24 @@ module Dynamoid
|
|
289
297
|
end
|
290
298
|
end
|
291
299
|
|
292
|
-
# string ->
|
300
|
+
# string -> StringIO
|
293
301
|
class BinaryDumper < Base
|
294
302
|
def process(value)
|
295
|
-
|
303
|
+
store_as_binary = if @options[:store_as_native_binary].nil?
|
304
|
+
Dynamoid.config.store_binary_as_native
|
305
|
+
else
|
306
|
+
@options[:store_as_native_binary]
|
307
|
+
end
|
308
|
+
|
309
|
+
if store_as_binary
|
310
|
+
if value.is_a?(StringIO) || value.is_a?(IO)
|
311
|
+
value
|
312
|
+
else
|
313
|
+
StringIO.new(value)
|
314
|
+
end
|
315
|
+
else
|
316
|
+
Base64.strict_encode64(value)
|
317
|
+
end
|
296
318
|
end
|
297
319
|
end
|
298
320
|
|
@@ -301,10 +323,10 @@ module Dynamoid
|
|
301
323
|
def process(value)
|
302
324
|
field_class = @options[:type]
|
303
325
|
|
304
|
-
if
|
305
|
-
value.dynamoid_dump
|
306
|
-
elsif field_class.respond_to?(:dynamoid_dump)
|
326
|
+
if field_class.respond_to?(:dynamoid_dump)
|
307
327
|
field_class.dynamoid_dump(value)
|
328
|
+
elsif value.respond_to?(:dynamoid_dump)
|
329
|
+
value.dynamoid_dump
|
308
330
|
else
|
309
331
|
raise ArgumentError, "Neither #{field_class} nor #{value} supports serialization for Dynamoid."
|
310
332
|
end
|
data/lib/dynamoid/errors.rb
CHANGED
@@ -6,6 +6,7 @@ module Dynamoid
|
|
6
6
|
# Generic Dynamoid error
|
7
7
|
class Error < StandardError; end
|
8
8
|
|
9
|
+
class MissingHashKey < Error; end
|
9
10
|
class MissingRangeKey < Error; end
|
10
11
|
|
11
12
|
class MissingIndex < Error; end
|
@@ -15,18 +16,27 @@ module Dynamoid
|
|
15
16
|
class InvalidIndex < Error
|
16
17
|
def initialize(item)
|
17
18
|
if item.is_a? String
|
18
|
-
super
|
19
|
+
super
|
19
20
|
else
|
20
21
|
super("Validation failed: #{item.errors.full_messages.join(', ')}")
|
21
22
|
end
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
26
|
+
class RecordNotSaved < Error
|
27
|
+
attr_reader :record
|
28
|
+
|
29
|
+
def initialize(record)
|
30
|
+
super('Failed to save the item')
|
31
|
+
@record = record
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
25
35
|
class RecordNotDestroyed < Error
|
26
36
|
attr_reader :record
|
27
37
|
|
28
38
|
def initialize(record)
|
29
|
-
super('Failed to destroy item')
|
39
|
+
super('Failed to destroy the item')
|
30
40
|
@record = record
|
31
41
|
end
|
32
42
|
end
|
@@ -76,8 +86,25 @@ module Dynamoid
|
|
76
86
|
|
77
87
|
class UnsupportedKeyType < Error; end
|
78
88
|
|
79
|
-
class UnknownAttribute < Error
|
89
|
+
class UnknownAttribute < Error
|
90
|
+
attr_reader :model_class, :attribute_name
|
91
|
+
|
92
|
+
def initialize(model_class, attribute_name)
|
93
|
+
super("Attribute #{attribute_name} does not exist in #{model_class}")
|
94
|
+
|
95
|
+
@model_class = model_class
|
96
|
+
@attribute_name = attribute_name
|
97
|
+
end
|
98
|
+
end
|
80
99
|
|
81
100
|
class SubclassNotFound < Error; end
|
101
|
+
|
102
|
+
class Rollback < Error; end
|
103
|
+
|
104
|
+
class ScanProhibited < Error
|
105
|
+
def initialize(_msg = nil)
|
106
|
+
super('Scan operations prohibited. Modify Dynamoid::Config.error_on_scan to change this behavior.')
|
107
|
+
end
|
108
|
+
end
|
82
109
|
end
|
83
110
|
end
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -197,9 +197,19 @@ module Dynamoid
|
|
197
197
|
# field :id, :integer
|
198
198
|
# end
|
199
199
|
#
|
200
|
+
# To declare a new attribute with not-default type as a table hash key a
|
201
|
+
# :key_type option can be used:
|
202
|
+
#
|
203
|
+
# class User
|
204
|
+
# include Dynamoid::Document
|
205
|
+
#
|
206
|
+
# table key: :user_id, key_type: :integer
|
207
|
+
# end
|
208
|
+
#
|
200
209
|
# @param options [Hash] options to override default table settings
|
201
210
|
# @option options [Symbol] :name name of a table
|
202
211
|
# @option options [Symbol] :key name of a hash key attribute
|
212
|
+
# @option options [Symbol] :key_type type of a hash key attribute
|
203
213
|
# @option options [Symbol] :inheritance_field name of an attribute used for STI
|
204
214
|
# @option options [Symbol] :capacity_mode table billing mode - either +provisioned+ or +on_demand+
|
205
215
|
# @option options [Integer] :write_capacity table write capacity units
|
@@ -210,11 +220,11 @@ module Dynamoid
|
|
210
220
|
# @since 0.4.0
|
211
221
|
def table(options)
|
212
222
|
self.options = options
|
213
|
-
|
214
223
|
# a default 'id' column is created when Dynamoid::Document is included
|
215
224
|
unless attributes.key? hash_key
|
216
225
|
remove_field :id
|
217
|
-
|
226
|
+
key_type = options[:key_type] || :string
|
227
|
+
field(hash_key, key_type)
|
218
228
|
end
|
219
229
|
|
220
230
|
# The created_at/updated_at fields are declared in the `included` callback first.
|
@@ -291,7 +301,7 @@ module Dynamoid
|
|
291
301
|
old_value = read_attribute(name)
|
292
302
|
|
293
303
|
unless attribute_is_present_on_model?(name)
|
294
|
-
raise Dynamoid::Errors::UnknownAttribute,
|
304
|
+
raise Dynamoid::Errors::UnknownAttribute.new(self.class, name)
|
295
305
|
end
|
296
306
|
|
297
307
|
if association = @associations[name]
|