dynamoid 3.11.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 +25 -3
- data/README.md +93 -13
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +8 -0
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +11 -3
- data/lib/dynamoid/dirty.rb +22 -11
- data/lib/dynamoid/dumping.rb +3 -3
- data/lib/dynamoid/errors.rb +16 -1
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +38 -17
- data/lib/dynamoid/persistence/inc.rb +30 -13
- data/lib/dynamoid/persistence/save.rb +24 -12
- data/lib/dynamoid/persistence/update_fields.rb +18 -5
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +17 -4
- data/lib/dynamoid/persistence.rb +146 -11
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +7 -2
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +7 -2
- data/lib/dynamoid/transaction_write/destroy.rb +7 -2
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +7 -2
- data/lib/dynamoid/transaction_write/update_fields.rb +169 -32
- data/lib/dynamoid/transaction_write/upsert.rb +12 -2
- data/lib/dynamoid/transaction_write.rb +212 -3
- data/lib/dynamoid/validations.rb +7 -4
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +1 -0
- metadata +8 -5
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dynamoid/transaction_read/find'
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
# The class +TransactionRead+ provides means to perform multiple reading
|
7
|
+
# operations in transaction, that is atomically, so that either all of them
|
8
|
+
# succeed, or all of them fail.
|
9
|
+
#
|
10
|
+
# The reading methods are supposed to be as close as possible to their
|
11
|
+
# non-transactional counterparts:
|
12
|
+
#
|
13
|
+
# user_id = params[:user_id]
|
14
|
+
# payment = params[:payment_id]
|
15
|
+
#
|
16
|
+
# models = Dynamoid::TransactionRead.execute do |t|
|
17
|
+
# t.find User, user_id
|
18
|
+
# t.find Payment, payment_id
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The only difference is that the methods are called on a transaction
|
22
|
+
# instance and a model or a model class should be specified. So +User.find+
|
23
|
+
# becomes +t.find(user_id)+ and +Payment.find(payment_id)+ becomes +t.find
|
24
|
+
# Payment, payment_id+.
|
25
|
+
#
|
26
|
+
# A transaction can be used without a block. This way a transaction instance
|
27
|
+
# should be instantiated and committed manually with +#commit+ method:
|
28
|
+
#
|
29
|
+
# t = Dynamoid::TransactionRead.new
|
30
|
+
#
|
31
|
+
# t.find user_id
|
32
|
+
# t.find payment_id
|
33
|
+
#
|
34
|
+
# models = t.commit
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# ### DynamoDB's transactions
|
38
|
+
#
|
39
|
+
# The main difference between DynamoDB transactions and a common interface is
|
40
|
+
# that DynamoDB's transactions are executed in batch. So in Dynamoid no
|
41
|
+
# data actually loaded when some transactional method (e.g+ `#find+) is
|
42
|
+
# called. All the changes are loaded at the end.
|
43
|
+
#
|
44
|
+
# A +TransactGetItems+ DynamoDB operation is used (see
|
45
|
+
# [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
|
46
|
+
# for details).
|
47
|
+
class TransactionRead
|
48
|
+
def self.execute
|
49
|
+
transaction = new
|
50
|
+
|
51
|
+
begin
|
52
|
+
yield transaction
|
53
|
+
transaction.commit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@actions = []
|
59
|
+
end
|
60
|
+
|
61
|
+
# Load all the models.
|
62
|
+
#
|
63
|
+
# transaction = Dynamoid::TransactionRead.new
|
64
|
+
# # ...
|
65
|
+
# transaction.commit
|
66
|
+
def commit
|
67
|
+
return [] if @actions.empty?
|
68
|
+
|
69
|
+
# some actions may produce multiple requests
|
70
|
+
action_request_groups = @actions.map(&:action_request).map do |action_request|
|
71
|
+
action_request.is_a?(Array) ? action_request : [action_request]
|
72
|
+
end
|
73
|
+
action_requests = action_request_groups.flatten(1)
|
74
|
+
|
75
|
+
return [] if action_requests.empty?
|
76
|
+
|
77
|
+
response = Dynamoid.adapter.transact_read_items(action_requests)
|
78
|
+
|
79
|
+
responses = response.responses.dup
|
80
|
+
@actions.zip(action_request_groups).map do |action, action_requests|
|
81
|
+
action_responses = responses.shift(action_requests.size)
|
82
|
+
action.process_responses(action_responses)
|
83
|
+
end.flatten
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find one or many objects, specified by one id or an array of ids.
|
87
|
+
#
|
88
|
+
# By default it raises +RecordNotFound+ exception if at least one model
|
89
|
+
# isn't found. This behavior can be changed with +raise_error+ option. If
|
90
|
+
# specified +raise_error: false+ option then +find+ will not raise the
|
91
|
+
# exception.
|
92
|
+
#
|
93
|
+
# When a document schema includes range key it should always be specified
|
94
|
+
# in +find+ method call. In case it's missing +MissingRangeKey+ exception
|
95
|
+
# will be raised.
|
96
|
+
#
|
97
|
+
# Please note that there are the following differences between
|
98
|
+
# transactional and non-transactional +find+:
|
99
|
+
# - transactional +find+ preserves order of models in result when given multiple ids
|
100
|
+
# - transactional +find+ doesn't return results immediately, a single
|
101
|
+
# collection with results of all the +find+ calls is returned instead
|
102
|
+
# - +:consistent_read+ option isn't supported
|
103
|
+
# - transactional +find+ is subject to limitations of the
|
104
|
+
# [TransactGetItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
|
105
|
+
# DynamoDB operation, e.g. the whole read transaction can load only up to 100 models.
|
106
|
+
#
|
107
|
+
# @param [Object|Array] ids a single primary key or an array of primary keys
|
108
|
+
# @param [Hash] options optional parameters of the operation
|
109
|
+
# @option options [Object] :range_key sort key of a model; required when a single partition key is given and a sort key is declared for a model
|
110
|
+
# @option options [true|false] :raise_error whether to raise a +RecordNotFound+ exception; specify explicitly +raise_error: false+ to suppress the exception; default is +true+
|
111
|
+
# @return [nil]
|
112
|
+
#
|
113
|
+
# @example Find by partition key
|
114
|
+
# Dynamoid::TransactionRead.execute do |t|
|
115
|
+
# t.find Document, 101
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# @example Find by partition key and sort key
|
119
|
+
# Dynamoid::TransactionRead.execute do |t|
|
120
|
+
# t.find Document, 101, range_key: 'archived'
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# @example Find several documents by partition key
|
124
|
+
# Dynamoid::TransactionRead.execute do |t|
|
125
|
+
# t.find Document, 101, 102, 103
|
126
|
+
# t.find Document, [101, 102, 103]
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# @example Find several documents by partition key and sort key
|
130
|
+
# Dynamoid::TransactionRead.execute do |t|
|
131
|
+
# t.find Document, [[101, 'archived'], [102, 'new'], [103, 'deleted']]
|
132
|
+
# end
|
133
|
+
def find(model_class, *ids, **options)
|
134
|
+
action = Dynamoid::TransactionRead::Find.new(model_class, *ids, **options)
|
135
|
+
register_action action
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def register_action(action)
|
141
|
+
@actions << action
|
142
|
+
action.on_registration
|
143
|
+
action.observable_by_user_result
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -35,10 +35,10 @@ module Dynamoid
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def action_request
|
38
|
-
key = { @model_class.hash_key => @model.hash_key }
|
38
|
+
key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
|
39
39
|
|
40
40
|
if @model_class.range_key?
|
41
|
-
key[@model_class.range_key] = @model.range_value
|
41
|
+
key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value)
|
42
42
|
end
|
43
43
|
|
44
44
|
{
|
@@ -55,6 +55,11 @@ module Dynamoid
|
|
55
55
|
raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
|
56
56
|
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
|
57
57
|
end
|
58
|
+
|
59
|
+
def dump_attribute(name, value)
|
60
|
+
options = @model_class.attributes[name]
|
61
|
+
Dumping.dump_field(value, options)
|
62
|
+
end
|
58
63
|
end
|
59
64
|
end
|
60
65
|
end
|
@@ -34,10 +34,10 @@ module Dynamoid
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def action_request
|
37
|
-
key = { @model_class.hash_key => @hash_key }
|
37
|
+
key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @hash_key) }
|
38
38
|
|
39
39
|
if @model_class.range_key?
|
40
|
-
key[@model_class.range_key] = @range_key
|
40
|
+
key[@model_class.range_key] = dump_attribute(@model_class.range_key, @range_key)
|
41
41
|
end
|
42
42
|
|
43
43
|
{
|
@@ -54,6 +54,11 @@ module Dynamoid
|
|
54
54
|
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
55
55
|
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
56
56
|
end
|
57
|
+
|
58
|
+
def dump_attribute(name, value)
|
59
|
+
options = @model_class.attributes[name]
|
60
|
+
Dumping.dump_field(value, options)
|
61
|
+
end
|
57
62
|
end
|
58
63
|
end
|
59
64
|
end
|
@@ -54,10 +54,10 @@ module Dynamoid
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def action_request
|
57
|
-
key = { @model_class.hash_key => @model.hash_key }
|
57
|
+
key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
|
58
58
|
|
59
59
|
if @model_class.range_key?
|
60
|
-
key[@model_class.range_key] = @model.range_value
|
60
|
+
key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value)
|
61
61
|
end
|
62
62
|
|
63
63
|
{
|
@@ -74,6 +74,11 @@ module Dynamoid
|
|
74
74
|
raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
|
75
75
|
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
|
76
76
|
end
|
77
|
+
|
78
|
+
def dump_attribute(name, value)
|
79
|
+
options = @model_class.attributes[name]
|
80
|
+
Dumping.dump_field(value, options)
|
81
|
+
end
|
77
82
|
end
|
78
83
|
end
|
79
84
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
class TransactionWrite
|
5
|
+
class ItemUpdater
|
6
|
+
attr_reader :attributes_to_set, :attributes_to_add, :attributes_to_delete, :attributes_to_remove
|
7
|
+
|
8
|
+
def initialize(model_class)
|
9
|
+
@model_class = model_class
|
10
|
+
|
11
|
+
@attributes_to_set = {}
|
12
|
+
@attributes_to_add = {}
|
13
|
+
@attributes_to_delete = {}
|
14
|
+
@attributes_to_remove = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
[@attributes_to_set, @attributes_to_add, @attributes_to_delete, @attributes_to_remove].all?(&:empty?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(attributes)
|
22
|
+
validate_attribute_names!(attributes.keys)
|
23
|
+
@attributes_to_set.merge!(attributes)
|
24
|
+
end
|
25
|
+
|
26
|
+
# adds to array of fields for use in REMOVE update expression
|
27
|
+
def remove(*names)
|
28
|
+
validate_attribute_names!(names)
|
29
|
+
@attributes_to_remove += names
|
30
|
+
end
|
31
|
+
|
32
|
+
# increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
|
33
|
+
def add(attributes)
|
34
|
+
validate_attribute_names!(attributes.keys)
|
35
|
+
@attributes_to_add.merge!(attributes)
|
36
|
+
end
|
37
|
+
|
38
|
+
# deletes a value or values from a set
|
39
|
+
def delete(attributes)
|
40
|
+
validate_attribute_names!(attributes.keys)
|
41
|
+
@attributes_to_delete.merge!(attributes)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def validate_attribute_names!(names)
|
47
|
+
names.each do |name|
|
48
|
+
unless @model_class.attributes[name]
|
49
|
+
raise Dynamoid::Errors::UnknownAttribute.new(@model_class, name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -121,8 +121,8 @@ module Dynamoid
|
|
121
121
|
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
122
122
|
|
123
123
|
# primary key to look up an item to update
|
124
|
-
key = { @model_class.hash_key => @model.hash_key }
|
125
|
-
key[@model_class.range_key] = @model.range_value if @model_class.range_key?
|
124
|
+
key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
|
125
|
+
key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
|
126
126
|
|
127
127
|
# Build UpdateExpression and keep names and values placeholders mapping
|
128
128
|
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
@@ -159,6 +159,11 @@ module Dynamoid
|
|
159
159
|
@model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
|
160
160
|
@model.created_at ||= timestamp unless skip_created_at
|
161
161
|
end
|
162
|
+
|
163
|
+
def dump_attribute(name, value)
|
164
|
+
options = @model_class.attributes[name]
|
165
|
+
Dumping.dump_field(value, options)
|
166
|
+
end
|
162
167
|
end
|
163
168
|
end
|
164
169
|
end
|
@@ -6,18 +6,24 @@ require 'dynamoid/persistence/update_validations'
|
|
6
6
|
module Dynamoid
|
7
7
|
class TransactionWrite
|
8
8
|
class UpdateFields < Base
|
9
|
-
def initialize(model_class, hash_key, range_key, attributes)
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes, &block)
|
10
10
|
super()
|
11
11
|
|
12
12
|
@model_class = model_class
|
13
13
|
@hash_key = hash_key
|
14
14
|
@range_key = range_key
|
15
|
-
@attributes = attributes
|
15
|
+
@attributes = attributes || {}
|
16
|
+
@block = block
|
16
17
|
end
|
17
18
|
|
18
19
|
def on_registration
|
19
20
|
validate_primary_key!
|
20
21
|
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
22
|
+
|
23
|
+
if @block
|
24
|
+
@item_updater = ItemUpdater.new(@model_class)
|
25
|
+
@block.call(@item_updater)
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
def on_commit; end
|
@@ -29,7 +35,7 @@ module Dynamoid
|
|
29
35
|
end
|
30
36
|
|
31
37
|
def skipped?
|
32
|
-
@attributes.empty?
|
38
|
+
@attributes.empty? && (!@item_updater || @item_updater.empty?)
|
33
39
|
end
|
34
40
|
|
35
41
|
def observable_by_user_result
|
@@ -37,48 +43,55 @@ module Dynamoid
|
|
37
43
|
end
|
38
44
|
|
39
45
|
def action_request
|
46
|
+
builder = UpdateRequestBuilder.new(@model_class)
|
47
|
+
|
48
|
+
# primary key to look up an item to update
|
49
|
+
builder.hash_key = dump_attribute(@model_class.hash_key, @hash_key)
|
50
|
+
builder.range_key = dump_attribute(@model_class.range_key, @range_key) if @model_class.range_key?
|
51
|
+
|
40
52
|
# changed attributes to persist
|
41
53
|
changes = @attributes.dup
|
42
54
|
changes = add_timestamps(changes, skip_created_at: true)
|
43
55
|
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
44
56
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
builder.set_attributes(changes_dumped)
|
58
|
+
|
59
|
+
# given a block
|
60
|
+
if @item_updater
|
61
|
+
builder.set_attributes(@item_updater.attributes_to_set)
|
62
|
+
builder.remove_attributes(@item_updater.attributes_to_remove)
|
63
|
+
|
64
|
+
@item_updater.attributes_to_add.each do |name, value|
|
65
|
+
# The ADD section in UpdateExpressions requires values to be a
|
66
|
+
# set to update a set attribute.
|
67
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
68
|
+
# Allow a single value not wrapped into a Set
|
69
|
+
if @model_class.attributes[name][:type] == :set
|
70
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
71
|
+
end
|
72
|
+
|
73
|
+
builder.add_value(name, value)
|
74
|
+
end
|
75
|
+
|
76
|
+
@item_updater.attributes_to_delete.each do |name, value|
|
77
|
+
# The DELETE section in UpdateExpressions requires values to be a
|
78
|
+
# set to update a set attribute.
|
79
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
80
|
+
# Allow a single value not wrapped into a Set
|
81
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
82
|
+
|
83
|
+
builder.delete_value(name, value)
|
84
|
+
end
|
62
85
|
end
|
63
86
|
|
64
|
-
update_expression = "SET #{update_expression_statements.join(', ')}"
|
65
|
-
|
66
87
|
# require primary key to exist
|
67
88
|
condition_expression = "attribute_exists(#{@model_class.hash_key})"
|
68
89
|
if @model_class.range_key?
|
69
90
|
condition_expression += " AND attribute_exists(#{@model_class.range_key})"
|
70
91
|
end
|
92
|
+
builder.condition_expression = condition_expression
|
71
93
|
|
72
|
-
|
73
|
-
update: {
|
74
|
-
key: key,
|
75
|
-
table_name: @model_class.table_name,
|
76
|
-
update_expression: update_expression,
|
77
|
-
expression_attribute_names: expression_attribute_names,
|
78
|
-
expression_attribute_values: expression_attribute_values,
|
79
|
-
condition_expression: condition_expression
|
80
|
-
}
|
81
|
-
}
|
94
|
+
builder.request
|
82
95
|
end
|
83
96
|
|
84
97
|
private
|
@@ -97,6 +110,130 @@ module Dynamoid
|
|
97
110
|
result[:updated_at] ||= timestamp
|
98
111
|
result
|
99
112
|
end
|
113
|
+
|
114
|
+
def dump_attribute(name, value)
|
115
|
+
options = @model_class.attributes[name]
|
116
|
+
Dumping.dump_field(value, options)
|
117
|
+
end
|
118
|
+
|
119
|
+
class UpdateRequestBuilder
|
120
|
+
attr_writer :hash_key, :range_key, :condition_expression
|
121
|
+
|
122
|
+
def initialize(model_class)
|
123
|
+
@model_class = model_class
|
124
|
+
|
125
|
+
@attributes_to_set = {}
|
126
|
+
@attributes_to_add = {}
|
127
|
+
@attributes_to_delete = {}
|
128
|
+
@attributes_to_remove = []
|
129
|
+
@condition_expression = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_attributes(attributes) # rubocop:disable Naming/AccessorMethodName
|
133
|
+
@attributes_to_set.merge!(attributes)
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_value(name, value)
|
137
|
+
@attributes_to_add[name] = value
|
138
|
+
end
|
139
|
+
|
140
|
+
def delete_value(name, value)
|
141
|
+
@attributes_to_delete[name] = value
|
142
|
+
end
|
143
|
+
|
144
|
+
def remove_attributes(names)
|
145
|
+
@attributes_to_remove.concat(names)
|
146
|
+
end
|
147
|
+
|
148
|
+
def request
|
149
|
+
key = { @model_class.hash_key => @hash_key }
|
150
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
151
|
+
|
152
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
153
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
154
|
+
update_expression_statements = []
|
155
|
+
expression_attribute_names = {}
|
156
|
+
expression_attribute_values = {}
|
157
|
+
name_placeholder = '#_n0'
|
158
|
+
value_placeholder = ':_v0'
|
159
|
+
|
160
|
+
unless @attributes_to_set.empty?
|
161
|
+
statements = []
|
162
|
+
|
163
|
+
@attributes_to_set.each do |name, value|
|
164
|
+
statements << "#{name_placeholder} = #{value_placeholder}"
|
165
|
+
|
166
|
+
expression_attribute_names[name_placeholder] = name
|
167
|
+
expression_attribute_values[value_placeholder] = value
|
168
|
+
|
169
|
+
name_placeholder = name_placeholder.succ
|
170
|
+
value_placeholder = value_placeholder.succ
|
171
|
+
end
|
172
|
+
|
173
|
+
update_expression_statements << "SET #{statements.join(', ')}"
|
174
|
+
end
|
175
|
+
|
176
|
+
unless @attributes_to_add.empty?
|
177
|
+
statements = []
|
178
|
+
|
179
|
+
@attributes_to_add.each do |name, value|
|
180
|
+
statements << "#{name_placeholder} #{value_placeholder}"
|
181
|
+
|
182
|
+
expression_attribute_names[name_placeholder] = name
|
183
|
+
expression_attribute_values[value_placeholder] = value
|
184
|
+
|
185
|
+
name_placeholder = name_placeholder.succ
|
186
|
+
value_placeholder = value_placeholder.succ
|
187
|
+
end
|
188
|
+
|
189
|
+
update_expression_statements << "ADD #{statements.join(', ')}"
|
190
|
+
end
|
191
|
+
|
192
|
+
unless @attributes_to_delete.empty?
|
193
|
+
statements = []
|
194
|
+
|
195
|
+
@attributes_to_delete.each do |name, value|
|
196
|
+
statements << "#{name_placeholder} #{value_placeholder}"
|
197
|
+
|
198
|
+
expression_attribute_names[name_placeholder] = name
|
199
|
+
expression_attribute_values[value_placeholder] = value
|
200
|
+
|
201
|
+
name_placeholder = name_placeholder.succ
|
202
|
+
value_placeholder = value_placeholder.succ
|
203
|
+
end
|
204
|
+
|
205
|
+
update_expression_statements << "DELETE #{statements.join(', ')}"
|
206
|
+
end
|
207
|
+
|
208
|
+
unless @attributes_to_remove.empty?
|
209
|
+
name_placeholders = []
|
210
|
+
|
211
|
+
@attributes_to_remove.each do |name|
|
212
|
+
name_placeholders << name_placeholder
|
213
|
+
|
214
|
+
expression_attribute_names[name_placeholder] = name
|
215
|
+
|
216
|
+
name_placeholder = name_placeholder.succ
|
217
|
+
value_placeholder = value_placeholder.succ
|
218
|
+
end
|
219
|
+
|
220
|
+
update_expression_statements << "REMOVE #{name_placeholders.join(', ')}"
|
221
|
+
end
|
222
|
+
|
223
|
+
update_expression = update_expression_statements.join(' ')
|
224
|
+
|
225
|
+
{
|
226
|
+
update: {
|
227
|
+
key: key,
|
228
|
+
table_name: @model_class.table_name,
|
229
|
+
update_expression: update_expression,
|
230
|
+
expression_attribute_names: expression_attribute_names,
|
231
|
+
expression_attribute_values: expression_attribute_values,
|
232
|
+
condition_expression: @condition_expression
|
233
|
+
}
|
234
|
+
}
|
235
|
+
end
|
236
|
+
end
|
100
237
|
end
|
101
238
|
end
|
102
239
|
end
|
@@ -44,8 +44,13 @@ module Dynamoid
|
|
44
44
|
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
45
45
|
|
46
46
|
# primary key to look up an item to update
|
47
|
-
|
48
|
-
key
|
47
|
+
partition_key_dumped = dump(@model_class.hash_key, @hash_key)
|
48
|
+
key = { @model_class.hash_key => partition_key_dumped }
|
49
|
+
|
50
|
+
if @model_class.range_key?
|
51
|
+
sort_key_dumped = dump(@model_class.range_key, @range_key)
|
52
|
+
key[@model_class.range_key] = sort_key_dumped
|
53
|
+
end
|
49
54
|
|
50
55
|
# Build UpdateExpression and keep names and values placeholders mapping
|
51
56
|
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
@@ -91,6 +96,11 @@ module Dynamoid
|
|
91
96
|
result[:updated_at] ||= timestamp
|
92
97
|
result
|
93
98
|
end
|
99
|
+
|
100
|
+
def dump(name, value)
|
101
|
+
options = @model_class.attributes[name]
|
102
|
+
Dumping.dump_field(value, options)
|
103
|
+
end
|
94
104
|
end
|
95
105
|
end
|
96
106
|
end
|