dynamoid 3.8.0 → 3.10.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 +54 -3
- data/README.md +111 -60
- data/SECURITY.md +17 -0
- data/dynamoid.gemspec +65 -0
- data/lib/dynamoid/adapter.rb +20 -13
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/execute_statement.rb +62 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +78 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +28 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +38 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +33 -27
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +116 -70
- data/lib/dynamoid/associations/belongs_to.rb +6 -6
- data/lib/dynamoid/associations.rb +1 -1
- data/lib/dynamoid/components.rb +2 -3
- data/lib/dynamoid/config/options.rb +12 -12
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +101 -138
- 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 +29 -0
- data/lib/dynamoid/dirty.rb +57 -57
- data/lib/dynamoid/document.rb +39 -3
- data/lib/dynamoid/dumping.rb +2 -2
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields/declare.rb +6 -6
- data/lib/dynamoid/fields.rb +9 -27
- data/lib/dynamoid/finders.rb +26 -30
- data/lib/dynamoid/indexes.rb +7 -10
- data/lib/dynamoid/loadable.rb +2 -2
- data/lib/dynamoid/log/formatter.rb +19 -4
- data/lib/dynamoid/persistence/import.rb +4 -1
- data/lib/dynamoid/persistence/inc.rb +66 -0
- data/lib/dynamoid/persistence/save.rb +55 -12
- data/lib/dynamoid/persistence/update_fields.rb +2 -2
- data/lib/dynamoid/persistence/update_validations.rb +2 -2
- data/lib/dynamoid/persistence.rb +128 -48
- data/lib/dynamoid/type_casting.rb +15 -14
- data/lib/dynamoid/undumping.rb +1 -1
- data/lib/dynamoid/version.rb +1 -1
- metadata +27 -49
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
# @private
|
5
|
+
module AdapterPlugin
|
6
|
+
class AwsSdkV3
|
7
|
+
class FilterExpressionConvertor
|
8
|
+
attr_reader :expression, :name_placeholders, :value_placeholders
|
9
|
+
|
10
|
+
def initialize(conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
11
|
+
@conditions = conditions
|
12
|
+
@name_placeholders = name_placeholders.dup
|
13
|
+
@value_placeholders = value_placeholders.dup
|
14
|
+
@name_placeholder_sequence = name_placeholder_sequence
|
15
|
+
@value_placeholder_sequence = value_placeholder_sequence
|
16
|
+
|
17
|
+
build
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def build
|
23
|
+
clauses = @conditions.map do |name, attribute_conditions|
|
24
|
+
attribute_conditions.map do |operator, value|
|
25
|
+
name_or_placeholder = name_or_placeholder_for(name)
|
26
|
+
|
27
|
+
case operator
|
28
|
+
when :eq
|
29
|
+
"#{name_or_placeholder} = #{value_placeholder_for(value)}"
|
30
|
+
when :ne
|
31
|
+
"#{name_or_placeholder} <> #{value_placeholder_for(value)}"
|
32
|
+
when :gt
|
33
|
+
"#{name_or_placeholder} > #{value_placeholder_for(value)}"
|
34
|
+
when :lt
|
35
|
+
"#{name_or_placeholder} < #{value_placeholder_for(value)}"
|
36
|
+
when :gte
|
37
|
+
"#{name_or_placeholder} >= #{value_placeholder_for(value)}"
|
38
|
+
when :lte
|
39
|
+
"#{name_or_placeholder} <= #{value_placeholder_for(value)}"
|
40
|
+
when :between
|
41
|
+
"#{name_or_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
|
42
|
+
when :begins_with
|
43
|
+
"begins_with (#{name_or_placeholder}, #{value_placeholder_for(value)})"
|
44
|
+
when :in
|
45
|
+
list = value.map(&method(:value_placeholder_for)).join(' , ')
|
46
|
+
"#{name_or_placeholder} IN (#{list})"
|
47
|
+
when :contains
|
48
|
+
"contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
|
49
|
+
when :not_contains
|
50
|
+
"NOT contains (#{name_or_placeholder}, #{value_placeholder_for(value)})"
|
51
|
+
when :null
|
52
|
+
"attribute_not_exists (#{name_or_placeholder})"
|
53
|
+
when :not_null
|
54
|
+
"attribute_exists (#{name_or_placeholder})"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end.flatten
|
58
|
+
|
59
|
+
@expression = clauses.join(' AND ')
|
60
|
+
end
|
61
|
+
|
62
|
+
def name_or_placeholder_for(name)
|
63
|
+
return name unless name.upcase.in?(Dynamoid::AdapterPlugin::AwsSdkV3::RESERVED_WORDS)
|
64
|
+
|
65
|
+
placeholder = @name_placeholder_sequence.call
|
66
|
+
@name_placeholders[placeholder] = name
|
67
|
+
placeholder
|
68
|
+
end
|
69
|
+
|
70
|
+
def value_placeholder_for(value)
|
71
|
+
placeholder = @value_placeholder_sequence.call
|
72
|
+
@value_placeholders[placeholder] = value
|
73
|
+
placeholder
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -50,7 +50,19 @@ module Dynamoid
|
|
50
50
|
# Replaces the values of one or more attributes
|
51
51
|
#
|
52
52
|
def set(values)
|
53
|
-
|
53
|
+
values_sanitized = sanitize_attributes(values)
|
54
|
+
|
55
|
+
if Dynamoid.config.store_attribute_with_nil_value
|
56
|
+
@updates.merge!(values_sanitized)
|
57
|
+
else
|
58
|
+
# delete explicitly attributes if assigned nil value and configured
|
59
|
+
# to not store nil values
|
60
|
+
values_to_update = values_sanitized.reject { |_, v| v.nil? }
|
61
|
+
values_to_delete = values_sanitized.select { |_, v| v.nil? }
|
62
|
+
|
63
|
+
@updates.merge!(values_to_update)
|
64
|
+
@deletions.merge!(values_to_delete)
|
65
|
+
end
|
54
66
|
end
|
55
67
|
|
56
68
|
#
|
@@ -85,10 +97,24 @@ module Dynamoid
|
|
85
97
|
|
86
98
|
private
|
87
99
|
|
100
|
+
# Keep in sync with AwsSdkV3.sanitize_item.
|
101
|
+
#
|
102
|
+
# The only difference is that to update item we need to track whether
|
103
|
+
# attribute value is nil or not.
|
88
104
|
def sanitize_attributes(attributes)
|
105
|
+
# rubocop:disable Lint/DuplicateBranch
|
89
106
|
attributes.transform_values do |v|
|
90
|
-
v.is_a?(Hash)
|
107
|
+
if v.is_a?(Hash)
|
108
|
+
v.stringify_keys
|
109
|
+
elsif v.is_a?(Set) && v.empty?
|
110
|
+
nil
|
111
|
+
elsif v.is_a?(String) && v.empty?
|
112
|
+
nil
|
113
|
+
else
|
114
|
+
v
|
115
|
+
end
|
91
116
|
end
|
117
|
+
# rubocop:enable Lint/DuplicateBranch
|
92
118
|
end
|
93
119
|
end
|
94
120
|
end
|
@@ -21,14 +21,17 @@ module Dynamoid
|
|
21
21
|
# lower to obey limits. We can assume the difference won't be
|
22
22
|
# negative due to break statements below but choose smaller limit
|
23
23
|
# which is why we have 2 separate if statements.
|
24
|
+
#
|
24
25
|
# NOTE: Adjusting based on record_limit can cause many HTTP requests
|
25
26
|
# being made. We may want to change this behavior, but it affects
|
26
27
|
# filtering on data with potentially large gaps.
|
28
|
+
#
|
27
29
|
# Example:
|
28
30
|
# User.where('created_at.gte' => 1.day.ago).record_limit(1000)
|
29
31
|
# Records 1-999 User's that fit criteria
|
30
32
|
# Records 1000-2000 Users's that do not fit criteria
|
31
33
|
# Record 2001 fits criteria
|
34
|
+
#
|
32
35
|
# The underlying implementation will have 1 page for records 1-999
|
33
36
|
# then will request with limit 1 for records 1000-2000 (making 1000
|
34
37
|
# requests of limit 1) until hit record 2001.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
# @private
|
5
|
+
module AdapterPlugin
|
6
|
+
class AwsSdkV3
|
7
|
+
class ProjectionExpressionConvertor
|
8
|
+
attr_reader :expression, :name_placeholders
|
9
|
+
|
10
|
+
def initialize(names, name_placeholders, name_placeholder_sequence)
|
11
|
+
@names = names
|
12
|
+
@name_placeholders = name_placeholders.dup
|
13
|
+
@name_placeholder_sequence = name_placeholder_sequence
|
14
|
+
|
15
|
+
build
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build
|
21
|
+
return if @names.nil? || @names.empty?
|
22
|
+
|
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
|
31
|
+
end
|
32
|
+
|
33
|
+
@expression = clauses.join(' , ')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative 'middleware/backoff'
|
4
4
|
require_relative 'middleware/limit'
|
5
5
|
require_relative 'middleware/start_key'
|
6
|
+
require_relative 'filter_expression_convertor'
|
7
|
+
require_relative 'projection_expression_convertor'
|
6
8
|
|
7
9
|
module Dynamoid
|
8
10
|
# @private
|
@@ -10,20 +12,19 @@ module Dynamoid
|
|
10
12
|
class AwsSdkV3
|
11
13
|
class Query
|
12
14
|
OPTIONS_KEYS = %i[
|
13
|
-
|
14
|
-
|
15
|
-
project
|
15
|
+
consistent_read scan_index_forward select index_name batch_size
|
16
|
+
exclusive_start_key record_limit scan_limit project
|
16
17
|
].freeze
|
17
18
|
|
18
19
|
attr_reader :client, :table, :options, :conditions
|
19
20
|
|
20
|
-
def initialize(client, table,
|
21
|
+
def initialize(client, table, key_conditions, non_key_conditions, options)
|
21
22
|
@client = client
|
22
23
|
@table = table
|
23
24
|
|
24
|
-
|
25
|
-
@
|
26
|
-
@
|
25
|
+
@key_conditions = key_conditions
|
26
|
+
@non_key_conditions = non_key_conditions
|
27
|
+
@options = options.slice(*OPTIONS_KEYS)
|
27
28
|
end
|
28
29
|
|
29
30
|
def call
|
@@ -53,6 +54,37 @@ module Dynamoid
|
|
53
54
|
private
|
54
55
|
|
55
56
|
def build_request
|
57
|
+
# expressions
|
58
|
+
name_placeholder = +'#_a0'
|
59
|
+
value_placeholder = +':_a0'
|
60
|
+
|
61
|
+
name_placeholder_sequence = -> { name_placeholder.next!.dup }
|
62
|
+
value_placeholder_sequence = -> { value_placeholder.next!.dup }
|
63
|
+
|
64
|
+
name_placeholders = {}
|
65
|
+
value_placeholders = {}
|
66
|
+
|
67
|
+
# Deal with various limits and batching
|
68
|
+
batch_size = options[:batch_size]
|
69
|
+
limit = [record_limit, scan_limit, batch_size].compact.min
|
70
|
+
|
71
|
+
# key condition expression
|
72
|
+
convertor = FilterExpressionConvertor.new(@key_conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
73
|
+
key_condition_expression = convertor.expression
|
74
|
+
value_placeholders = convertor.value_placeholders
|
75
|
+
name_placeholders = convertor.name_placeholders
|
76
|
+
|
77
|
+
# filter expression
|
78
|
+
convertor = FilterExpressionConvertor.new(@non_key_conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
79
|
+
filter_expression = convertor.expression
|
80
|
+
value_placeholders = convertor.value_placeholders
|
81
|
+
name_placeholders = convertor.name_placeholders
|
82
|
+
|
83
|
+
# projection expression
|
84
|
+
convertor = ProjectionExpressionConvertor.new(options[:project], name_placeholders, name_placeholder_sequence)
|
85
|
+
projection_expression = convertor.expression
|
86
|
+
name_placeholders = convertor.name_placeholders
|
87
|
+
|
56
88
|
request = options.slice(
|
57
89
|
:consistent_read,
|
58
90
|
:scan_index_forward,
|
@@ -61,15 +93,13 @@ module Dynamoid
|
|
61
93
|
:exclusive_start_key
|
62
94
|
).compact
|
63
95
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
request[:
|
69
|
-
request[:
|
70
|
-
request[:
|
71
|
-
request[:query_filter] = query_filter
|
72
|
-
request[:attributes_to_get] = attributes_to_get
|
96
|
+
request[:table_name] = table.name
|
97
|
+
request[:limit] = limit if limit
|
98
|
+
request[:key_condition_expression] = key_condition_expression if key_condition_expression.present?
|
99
|
+
request[:filter_expression] = filter_expression if filter_expression.present?
|
100
|
+
request[:expression_attribute_values] = value_placeholders if value_placeholders.present?
|
101
|
+
request[:expression_attribute_names] = name_placeholders if name_placeholders.present?
|
102
|
+
request[:projection_expression] = projection_expression if projection_expression.present?
|
73
103
|
|
74
104
|
request
|
75
105
|
end
|
@@ -81,51 +111,6 @@ module Dynamoid
|
|
81
111
|
def scan_limit
|
82
112
|
options[:scan_limit]
|
83
113
|
end
|
84
|
-
|
85
|
-
def hash_key_name
|
86
|
-
(options[:hash_key] || table.hash_key)
|
87
|
-
end
|
88
|
-
|
89
|
-
def range_key_name
|
90
|
-
(options[:range_key] || table.range_key)
|
91
|
-
end
|
92
|
-
|
93
|
-
def key_conditions
|
94
|
-
result = {
|
95
|
-
hash_key_name => {
|
96
|
-
comparison_operator: AwsSdkV3::EQ,
|
97
|
-
attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::EQ, options[:hash_value].freeze)
|
98
|
-
}
|
99
|
-
}
|
100
|
-
|
101
|
-
conditions.slice(*AwsSdkV3::RANGE_MAP.keys).each do |k, _v|
|
102
|
-
op = AwsSdkV3::RANGE_MAP[k]
|
103
|
-
|
104
|
-
result[range_key_name] = {
|
105
|
-
comparison_operator: op,
|
106
|
-
attribute_value_list: AwsSdkV3.attribute_value_list(op, conditions[k].freeze)
|
107
|
-
}
|
108
|
-
end
|
109
|
-
|
110
|
-
result
|
111
|
-
end
|
112
|
-
|
113
|
-
def query_filter
|
114
|
-
conditions.except(*AwsSdkV3::RANGE_MAP.keys).reduce({}) do |result, (attr, cond)|
|
115
|
-
condition = {
|
116
|
-
comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
|
117
|
-
attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
|
118
|
-
}
|
119
|
-
result[attr] = condition
|
120
|
-
result
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def attributes_to_get
|
125
|
-
return if options[:project].nil?
|
126
|
-
|
127
|
-
options[:project].map(&:to_s)
|
128
|
-
end
|
129
114
|
end
|
130
115
|
end
|
131
116
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative 'middleware/backoff'
|
4
4
|
require_relative 'middleware/limit'
|
5
5
|
require_relative 'middleware/start_key'
|
6
|
+
require_relative 'filter_expression_convertor'
|
7
|
+
require_relative 'projection_expression_convertor'
|
6
8
|
|
7
9
|
module Dynamoid
|
8
10
|
# @private
|
@@ -45,6 +47,31 @@ module Dynamoid
|
|
45
47
|
private
|
46
48
|
|
47
49
|
def build_request
|
50
|
+
# expressions
|
51
|
+
name_placeholder = +'#_a0'
|
52
|
+
value_placeholder = +':_a0'
|
53
|
+
|
54
|
+
name_placeholder_sequence = -> { name_placeholder.next!.dup }
|
55
|
+
value_placeholder_sequence = -> { value_placeholder.next!.dup }
|
56
|
+
|
57
|
+
name_placeholders = {}
|
58
|
+
value_placeholders = {}
|
59
|
+
|
60
|
+
# Deal with various limits and batching
|
61
|
+
batch_size = options[:batch_size]
|
62
|
+
limit = [record_limit, scan_limit, batch_size].compact.min
|
63
|
+
|
64
|
+
# filter expression
|
65
|
+
convertor = FilterExpressionConvertor.new(conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
66
|
+
filter_expression = convertor.expression
|
67
|
+
value_placeholders = convertor.value_placeholders
|
68
|
+
name_placeholders = convertor.name_placeholders
|
69
|
+
|
70
|
+
# projection expression
|
71
|
+
convertor = ProjectionExpressionConvertor.new(options[:project], name_placeholders, name_placeholder_sequence)
|
72
|
+
projection_expression = convertor.expression
|
73
|
+
name_placeholders = convertor.name_placeholders
|
74
|
+
|
48
75
|
request = options.slice(
|
49
76
|
:consistent_read,
|
50
77
|
:exclusive_start_key,
|
@@ -52,14 +79,12 @@ module Dynamoid
|
|
52
79
|
:index_name
|
53
80
|
).compact
|
54
81
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
request[:
|
60
|
-
request[:
|
61
|
-
request[:scan_filter] = scan_filter
|
62
|
-
request[:attributes_to_get] = attributes_to_get
|
82
|
+
request[:table_name] = table.name
|
83
|
+
request[:limit] = limit if limit
|
84
|
+
request[:filter_expression] = filter_expression if filter_expression.present?
|
85
|
+
request[:expression_attribute_values] = value_placeholders if value_placeholders.present?
|
86
|
+
request[:expression_attribute_names] = name_placeholders if name_placeholders.present?
|
87
|
+
request[:projection_expression] = projection_expression if projection_expression.present?
|
63
88
|
|
64
89
|
request
|
65
90
|
end
|
@@ -71,25 +96,6 @@ module Dynamoid
|
|
71
96
|
def scan_limit
|
72
97
|
options[:scan_limit]
|
73
98
|
end
|
74
|
-
|
75
|
-
def scan_filter
|
76
|
-
conditions.reduce({}) do |result, (attr, cond)|
|
77
|
-
condition = {
|
78
|
-
comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
|
79
|
-
attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
|
80
|
-
}
|
81
|
-
# nil means operator doesn't require attribute value list
|
82
|
-
conditions.delete(:attribute_value_list) if conditions[:attribute_value_list].nil?
|
83
|
-
result[attr] = condition
|
84
|
-
result
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def attributes_to_get
|
89
|
-
return if options[:project].nil?
|
90
|
-
|
91
|
-
options[:project].map(&:to_s)
|
92
|
-
end
|
93
99
|
end
|
94
100
|
end
|
95
101
|
end
|