Skip to content

Commit 883ca94

Browse files
committed
Add specs and refactor implementation
1 parent 9b2145a commit 883ca94

File tree

4 files changed

+746
-315
lines changed

4 files changed

+746
-315
lines changed

lib/dynamoid/transaction_write.rb

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,51 @@ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable
347347
# end
348348
#
349349
# Updates can also be performed in a block.
350+
#
350351
# Dynamoid::TransactionWrite.execute do |t|
351352
# t.update_fields(User, 1) do |u|
352-
# u.set age: 27, last_name: 'Tylor'
353-
# u.add article_count: 1 # increment a counter
354-
# u.delete favorite_colors: 'green' # remove from a set
355-
# u.delete :first_name # clear a field
353+
# u.add(article_count: 1)
354+
# u.delete(favorite_colors: 'green')
355+
# u.set(age: 27, last_name: 'Tylor')
356356
# end
357357
# end
358358
#
359+
# Operation +add+ just adds a value for numeric attributes and join
360+
# collections if attribute is a set.
361+
#
362+
# t.update_fields(User, 1) do |u|
363+
# u.add(age: 1, followers_count: 5)
364+
# u.add(hobbies: ['skying', 'climbing'])
365+
# end
366+
#
367+
# Operation +delete+ is applied to collection attribute types and
368+
# substructs one collection from another.
369+
#
370+
# t.update_fields(User, 1) do |u|
371+
# u.delete(hobbies: ['skying'])
372+
# end
373+
#
374+
# Operation +set+ just changes an attribute value:
375+
#
376+
# t.update_fields(User, 1) do |u|
377+
# u.set(age: 21)
378+
# end
379+
#
380+
# Operation +remove+ removes one or more attributes from an item.
381+
#
382+
# t.update_fields(User, 1) do |u|
383+
# u.remove(:age)
384+
# end
385+
#
386+
# All the operations work like +ADD+, +DELETE+, +REMOVE+, and +SET+ actions supported
387+
# by +UpdateExpression+
388+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html]
389+
# of +UpdateItem+ operation.
390+
#
391+
# It's atomic operations. So adding or deleting elements in a collection
392+
# or incrementing or decrementing a numeric field is atomic and does not
393+
# interfere with other write requests.
394+
#
359395
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
360396
# attributes is not declared in the model class.
361397
#
@@ -364,14 +400,14 @@ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable
364400
# @param range_key [Scalar value] range key value (optional)
365401
# @param attributes [Hash]
366402
# @return [nil]
367-
def update_fields(model_class, hash_key, range_key = nil, attributes = nil)
368-
if attributes.nil? && range_key.is_a?(Hash)
403+
def update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block)
404+
# given no attributes, but there may be a block
405+
if range_key.is_a?(Hash) && !attributes
369406
attributes = range_key
370407
range_key = nil
371408
end
372-
action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
373-
# cannot pass in &block due to code climate max-args limit so instead yield it here
374-
yield(action) if block_given?
409+
410+
action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes, &block)
375411
register_action action
376412
end
377413

lib/dynamoid/transaction_write/item_updater.rb

Lines changed: 30 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,106 +3,53 @@
33
module Dynamoid
44
class TransactionWrite
55
class ItemUpdater
6-
def initialize
7-
@additions = {}
8-
@deletions = {}
9-
@removals = []
10-
end
6+
attr_reader :attributes_to_set, :attributes_to_add, :attributes_to_delete, :attributes_to_remove
117

12-
def empty?
13-
@additions.empty? && @deletions.empty? && @removals.empty?
14-
end
8+
def initialize(model_class)
9+
@model_class = model_class
1510

16-
# adds to array of fields for use in REMOVE update expression
17-
def remove(field)
18-
@removals << field
19-
end
20-
21-
# increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
22-
def add(values)
23-
@additions.merge!(values)
11+
@attributes_to_set = {}
12+
@attributes_to_add = {}
13+
@attributes_to_delete = {}
14+
@attributes_to_remove = []
2415
end
2516

26-
# deletes a value or values from a set type or simply sets a field to nil
27-
def delete(field_or_values)
28-
if field_or_values.is_a?(Hash)
29-
@deletions.merge!(field_or_values)
30-
else
31-
remove(field_or_values)
32-
end
33-
end
34-
35-
def merge_update_expression(expression_attribute_names, expression_attribute_values, update_expression)
36-
update_expression = set_additions(expression_attribute_values, update_expression)
37-
update_expression = set_deletions(expression_attribute_values, update_expression)
38-
expression_attribute_names, update_expression = set_removals(expression_attribute_names, update_expression)
39-
[expression_attribute_names, update_expression]
40-
end
41-
42-
# adds all of the ADD statements to the update_expression and returns it
43-
def set_additions(expression_attribute_values, update_expression)
44-
return update_expression unless @additions.present?
45-
46-
# ADD statements can be used to increment a counter:
47-
# txn.update!(UserCount, "UserCount#Red", {}, options: {add: {record_count: 1}})
48-
add_keys = @additions.keys
49-
add_values = transform_add_values(@additions)
50-
51-
update_expression += build_add_expression(add_keys)
52-
add_keys.each_with_index { |k, i| expression_attribute_values[":_a#{i}"] = add_values[k] }
53-
update_expression
17+
def empty?
18+
[@attributes_to_set, @attributes_to_add, @attributes_to_delete, @attributes_to_remove].all?(&:empty?)
5419
end
5520

56-
def transform_add_values(additions)
57-
additions.transform_values do |v|
58-
!v.is_a?(Set) && v.is_a?(Enumerable) ? Set.new(v) : v
59-
end
21+
def set(attributes)
22+
validate_attribute_names!(attributes.keys)
23+
@attributes_to_set.merge!(attributes)
6024
end
6125

62-
def build_add_expression(add_keys)
63-
" ADD #{add_keys.each_with_index.map { |k, i| "#{k} :_a#{i}" }.join(', ')}"
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
6430
end
6531

66-
# adds all of the DELETE statements to the update_expression and returns it
67-
def set_deletions(expression_attribute_values, update_expression)
68-
return update_expression unless @deletions.present?
69-
70-
update_expression += build_delete_expression(@deletions.keys)
71-
delete_values = prepare_delete_values(@deletions)
72-
73-
@deletions.keys.each_with_index do |key, index|
74-
expression_attribute_values[":_d#{index}"] = delete_values[key]
75-
end
76-
77-
update_expression
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)
7836
end
7937

80-
def build_delete_expression(delete_keys)
81-
" DELETE #{delete_keys.each_with_index.map { |key, index| "#{key} :_d#{index}" }.join(', ')}"
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)
8242
end
8343

84-
def prepare_delete_values(deletions)
85-
deletions.transform_values(&method(:prepare_delete_values_transform))
86-
end
44+
private
8745

88-
def prepare_delete_values_transform(value)
89-
if value.is_a?(Set)
90-
value
91-
else
92-
Set.new(value.is_a?(Enumerable) ? value : [value])
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
9351
end
9452
end
95-
96-
# adds all of the removals as a REMOVE clause
97-
def set_removals(expression_attribute_names, update_expression)
98-
return expression_attribute_names, update_expression unless @removals.present?
99-
100-
update_expression += " REMOVE #{@removals.each_with_index.map { |_k, i| "#_r#{i}" }.join(', ')}"
101-
expression_attribute_names = expression_attribute_names.merge(
102-
@removals.each_with_index.map { |k, i| ["#_r#{i}", k.to_s] }.to_h
103-
)
104-
[expression_attribute_names, update_expression]
105-
end
10653
end
10754
end
10855
end

0 commit comments

Comments
 (0)