dynamoid 3.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -6
- data/README.md +202 -25
- data/dynamoid.gemspec +5 -6
- data/lib/dynamoid/adapter.rb +19 -13
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
- data/lib/dynamoid/associations/belongs_to.rb +6 -6
- data/lib/dynamoid/associations.rb +1 -1
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config/options.rb +12 -12
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +149 -142
- data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
- data/lib/dynamoid/criteria/where_conditions.rb +36 -0
- data/lib/dynamoid/dirty.rb +87 -12
- data/lib/dynamoid/document.rb +1 -1
- data/lib/dynamoid/dumping.rb +38 -16
- data/lib/dynamoid/errors.rb +14 -2
- data/lib/dynamoid/fields/declare.rb +6 -6
- data/lib/dynamoid/fields.rb +6 -8
- data/lib/dynamoid/finders.rb +23 -32
- data/lib/dynamoid/indexes.rb +6 -7
- data/lib/dynamoid/loadable.rb +3 -2
- data/lib/dynamoid/persistence/inc.rb +6 -7
- 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 +17 -18
- data/lib/dynamoid/persistence/update_fields.rb +7 -5
- data/lib/dynamoid/persistence/update_validations.rb +1 -1
- data/lib/dynamoid/persistence/upsert.rb +5 -4
- data/lib/dynamoid/persistence.rb +77 -21
- 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 +60 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
- data/lib/dynamoid/transaction_write/destroy.rb +79 -0
- data/lib/dynamoid/transaction_write/save.rb +164 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
- data/lib/dynamoid/transaction_write/upsert.rb +96 -0
- data/lib/dynamoid/transaction_write.rb +464 -0
- data/lib/dynamoid/type_casting.rb +18 -15
- data/lib/dynamoid/undumping.rb +14 -3
- data/lib/dynamoid/validations.rb +1 -1
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +7 -0
- metadata +30 -16
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
class TransactionWrite
|
7
|
+
class Destroy < Base
|
8
|
+
def initialize(model, **options)
|
9
|
+
super()
|
10
|
+
|
11
|
+
@model = model
|
12
|
+
@options = options
|
13
|
+
@model_class = model.class
|
14
|
+
@aborted = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_registration
|
18
|
+
validate_model!
|
19
|
+
|
20
|
+
@aborted = true
|
21
|
+
@model.run_callbacks(:destroy) do
|
22
|
+
@aborted = false
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
if @aborted && @options[:raise_error]
|
27
|
+
raise Dynamoid::Errors::RecordNotDestroyed, @model
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_commit
|
32
|
+
return if @aborted
|
33
|
+
|
34
|
+
@model.destroyed = true
|
35
|
+
@model.run_callbacks(:commit)
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_rollback
|
39
|
+
@model.run_callbacks(:rollback)
|
40
|
+
end
|
41
|
+
|
42
|
+
def aborted?
|
43
|
+
@aborted
|
44
|
+
end
|
45
|
+
|
46
|
+
def skipped?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def observable_by_user_result
|
51
|
+
return false if @aborted
|
52
|
+
|
53
|
+
@model
|
54
|
+
end
|
55
|
+
|
56
|
+
def action_request
|
57
|
+
key = { @model_class.hash_key => @model.hash_key }
|
58
|
+
|
59
|
+
if @model_class.range_key?
|
60
|
+
key[@model_class.range_key] = @model.range_value
|
61
|
+
end
|
62
|
+
|
63
|
+
{
|
64
|
+
delete: {
|
65
|
+
key: key,
|
66
|
+
table_name: @model_class.table_name
|
67
|
+
}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def validate_model!
|
74
|
+
raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
|
75
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
class TransactionWrite
|
7
|
+
class Save < Base
|
8
|
+
def initialize(model, **options)
|
9
|
+
super()
|
10
|
+
|
11
|
+
@model = model
|
12
|
+
@model_class = model.class
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
@aborted = false
|
16
|
+
@was_new_record = model.new_record?
|
17
|
+
@valid = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_registration
|
21
|
+
validate_model!
|
22
|
+
|
23
|
+
if @options[:validate] != false && !(@valid = @model.valid?)
|
24
|
+
if @options[:raise_error]
|
25
|
+
raise Dynamoid::Errors::DocumentNotValid, @model
|
26
|
+
else
|
27
|
+
@aborted = true
|
28
|
+
return
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@aborted = true
|
33
|
+
callback_name = @was_new_record ? :create : :update
|
34
|
+
|
35
|
+
@model.run_callbacks(:save) do
|
36
|
+
@model.run_callbacks(callback_name) do
|
37
|
+
@model.run_callbacks(:validate) do
|
38
|
+
@aborted = false
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if @aborted && @options[:raise_error]
|
45
|
+
raise Dynamoid::Errors::RecordNotSaved, @model
|
46
|
+
end
|
47
|
+
|
48
|
+
if @was_new_record && @model.hash_key.nil?
|
49
|
+
@model.hash_key = SecureRandom.uuid
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_commit
|
54
|
+
return if @aborted
|
55
|
+
|
56
|
+
@model.changes_applied
|
57
|
+
|
58
|
+
if @was_new_record
|
59
|
+
@model.new_record = false
|
60
|
+
end
|
61
|
+
|
62
|
+
@model.run_callbacks(:commit)
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_rollback
|
66
|
+
@model.run_callbacks(:rollback)
|
67
|
+
end
|
68
|
+
|
69
|
+
def aborted?
|
70
|
+
@aborted
|
71
|
+
end
|
72
|
+
|
73
|
+
def skipped?
|
74
|
+
@model.persisted? && !@model.changed?
|
75
|
+
end
|
76
|
+
|
77
|
+
def observable_by_user_result
|
78
|
+
!@aborted
|
79
|
+
end
|
80
|
+
|
81
|
+
def action_request
|
82
|
+
if @was_new_record
|
83
|
+
action_request_to_create
|
84
|
+
else
|
85
|
+
action_request_to_update
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def validate_model!
|
92
|
+
raise Dynamoid::Errors::MissingHashKey if !@was_new_record && @model.hash_key.nil?
|
93
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
def action_request_to_create
|
97
|
+
touch_model_timestamps(skip_created_at: false)
|
98
|
+
|
99
|
+
attributes_dumped = Dynamoid::Dumping.dump_attributes(@model.attributes, @model_class.attributes)
|
100
|
+
|
101
|
+
# require primary key not to exist yet
|
102
|
+
condition = "attribute_not_exists(#{@model_class.hash_key})"
|
103
|
+
if @model_class.range_key?
|
104
|
+
condition += " AND attribute_not_exists(#{@model_class.range_key})"
|
105
|
+
end
|
106
|
+
|
107
|
+
{
|
108
|
+
put: {
|
109
|
+
item: attributes_dumped,
|
110
|
+
table_name: @model_class.table_name,
|
111
|
+
condition_expression: condition
|
112
|
+
}
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def action_request_to_update
|
117
|
+
touch_model_timestamps(skip_created_at: true)
|
118
|
+
|
119
|
+
# changed attributes to persist
|
120
|
+
changes = @model.attributes.slice(*@model.changed.map(&:to_sym))
|
121
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
122
|
+
|
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?
|
126
|
+
|
127
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
128
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
129
|
+
update_expression_statements = []
|
130
|
+
expression_attribute_names = {}
|
131
|
+
expression_attribute_values = {}
|
132
|
+
|
133
|
+
changes_dumped.each_with_index do |(name, value), i|
|
134
|
+
name_placeholder = "#_n#{i}"
|
135
|
+
value_placeholder = ":_s#{i}"
|
136
|
+
|
137
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
138
|
+
expression_attribute_names[name_placeholder] = name
|
139
|
+
expression_attribute_values[value_placeholder] = value
|
140
|
+
end
|
141
|
+
|
142
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
143
|
+
|
144
|
+
{
|
145
|
+
update: {
|
146
|
+
key: key,
|
147
|
+
table_name: @model_class.table_name,
|
148
|
+
update_expression: update_expression,
|
149
|
+
expression_attribute_names: expression_attribute_names,
|
150
|
+
expression_attribute_values: expression_attribute_values
|
151
|
+
}
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def touch_model_timestamps(skip_created_at:)
|
156
|
+
return unless @model_class.timestamps_enabled?
|
157
|
+
|
158
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
159
|
+
@model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
|
160
|
+
@model.created_at ||= timestamp unless skip_created_at
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class UpdateAttributes < Base
|
9
|
+
def initialize(model, attributes, **options)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model = model
|
13
|
+
@model.assign_attributes(attributes)
|
14
|
+
@save_action = Save.new(model, **options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_registration
|
18
|
+
@save_action.on_registration
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_commit
|
22
|
+
@save_action.on_commit
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_rollback
|
26
|
+
@save_action.on_rollback
|
27
|
+
end
|
28
|
+
|
29
|
+
def aborted?
|
30
|
+
@save_action.aborted?
|
31
|
+
end
|
32
|
+
|
33
|
+
def skipped?
|
34
|
+
@save_action.skipped?
|
35
|
+
end
|
36
|
+
|
37
|
+
def observable_by_user_result
|
38
|
+
@save_action.observable_by_user_result
|
39
|
+
end
|
40
|
+
|
41
|
+
def action_request
|
42
|
+
@save_action.action_request
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class UpdateFields < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_registration
|
19
|
+
validate_primary_key!
|
20
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_commit; end
|
24
|
+
|
25
|
+
def on_rollback; end
|
26
|
+
|
27
|
+
def aborted?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def skipped?
|
32
|
+
@attributes.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def observable_by_user_result
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def action_request
|
40
|
+
# changed attributes to persist
|
41
|
+
changes = @attributes.dup
|
42
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
43
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
44
|
+
|
45
|
+
# primary key to look up an item to update
|
46
|
+
key = { @model_class.hash_key => @hash_key }
|
47
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
48
|
+
|
49
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
50
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
51
|
+
update_expression_statements = []
|
52
|
+
expression_attribute_names = {}
|
53
|
+
expression_attribute_values = {}
|
54
|
+
|
55
|
+
changes_dumped.each_with_index do |(name, value), i|
|
56
|
+
name_placeholder = "#_n#{i}"
|
57
|
+
value_placeholder = ":_s#{i}"
|
58
|
+
|
59
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
60
|
+
expression_attribute_names[name_placeholder] = name
|
61
|
+
expression_attribute_values[value_placeholder] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
65
|
+
|
66
|
+
# require primary key to exist
|
67
|
+
condition_expression = "attribute_exists(#{@model_class.hash_key})"
|
68
|
+
if @model_class.range_key?
|
69
|
+
condition_expression += " AND attribute_exists(#{@model_class.range_key})"
|
70
|
+
end
|
71
|
+
|
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
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def validate_primary_key!
|
87
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
88
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_timestamps(attributes, skip_created_at: false)
|
92
|
+
return attributes unless @model_class.timestamps_enabled?
|
93
|
+
|
94
|
+
result = attributes.clone
|
95
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
96
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
97
|
+
result[:updated_at] ||= timestamp
|
98
|
+
result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class Upsert < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_registration
|
19
|
+
validate_primary_key!
|
20
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_commit; end
|
24
|
+
|
25
|
+
def on_rollback; end
|
26
|
+
|
27
|
+
def aborted?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def skipped?
|
32
|
+
attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
|
33
|
+
attributes_to_assign.empty? && !@model_class.timestamps_enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
def observable_by_user_result
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def action_request
|
41
|
+
# changed attributes to persist
|
42
|
+
changes = @attributes.dup
|
43
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
44
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
45
|
+
|
46
|
+
# primary key to look up an item to update
|
47
|
+
key = { @model_class.hash_key => @hash_key }
|
48
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
49
|
+
|
50
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
51
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
52
|
+
update_expression_statements = []
|
53
|
+
expression_attribute_names = {}
|
54
|
+
expression_attribute_values = {}
|
55
|
+
|
56
|
+
changes_dumped.each_with_index do |(name, value), i|
|
57
|
+
name_placeholder = "#_n#{i}"
|
58
|
+
value_placeholder = ":_s#{i}"
|
59
|
+
|
60
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
61
|
+
expression_attribute_names[name_placeholder] = name
|
62
|
+
expression_attribute_values[value_placeholder] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
66
|
+
|
67
|
+
{
|
68
|
+
update: {
|
69
|
+
key: key,
|
70
|
+
table_name: @model_class.table_name,
|
71
|
+
update_expression: update_expression,
|
72
|
+
expression_attribute_names: expression_attribute_names,
|
73
|
+
expression_attribute_values: expression_attribute_values
|
74
|
+
}
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def validate_primary_key!
|
81
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
82
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_timestamps(attributes, skip_created_at: false)
|
86
|
+
return attributes unless @model_class.timestamps_enabled?
|
87
|
+
|
88
|
+
result = attributes.clone
|
89
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
90
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
91
|
+
result[:updated_at] ||= timestamp
|
92
|
+
result
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|