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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -3
  3. data/README.md +111 -60
  4. data/SECURITY.md +17 -0
  5. data/dynamoid.gemspec +65 -0
  6. data/lib/dynamoid/adapter.rb +20 -13
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/execute_statement.rb +62 -0
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +78 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +28 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +3 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +38 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +33 -27
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +116 -70
  16. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  17. data/lib/dynamoid/associations.rb +1 -1
  18. data/lib/dynamoid/components.rb +2 -3
  19. data/lib/dynamoid/config/options.rb +12 -12
  20. data/lib/dynamoid/config.rb +1 -0
  21. data/lib/dynamoid/criteria/chain.rb +101 -138
  22. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  23. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  24. data/lib/dynamoid/criteria/where_conditions.rb +29 -0
  25. data/lib/dynamoid/dirty.rb +57 -57
  26. data/lib/dynamoid/document.rb +39 -3
  27. data/lib/dynamoid/dumping.rb +2 -2
  28. data/lib/dynamoid/errors.rb +2 -0
  29. data/lib/dynamoid/fields/declare.rb +6 -6
  30. data/lib/dynamoid/fields.rb +9 -27
  31. data/lib/dynamoid/finders.rb +26 -30
  32. data/lib/dynamoid/indexes.rb +7 -10
  33. data/lib/dynamoid/loadable.rb +2 -2
  34. data/lib/dynamoid/log/formatter.rb +19 -4
  35. data/lib/dynamoid/persistence/import.rb +4 -1
  36. data/lib/dynamoid/persistence/inc.rb +66 -0
  37. data/lib/dynamoid/persistence/save.rb +55 -12
  38. data/lib/dynamoid/persistence/update_fields.rb +2 -2
  39. data/lib/dynamoid/persistence/update_validations.rb +2 -2
  40. data/lib/dynamoid/persistence.rb +128 -48
  41. data/lib/dynamoid/type_casting.rb +15 -14
  42. data/lib/dynamoid/undumping.rb +1 -1
  43. data/lib/dynamoid/version.rb +1 -1
  44. metadata +27 -49
  45. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  46. 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
- @updates.merge!(sanitize_attributes(values))
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) ? v.stringify_keys : v
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
- limit hash_key hash_value range_key consistent_read scan_index_forward
14
- select index_name batch_size exclusive_start_key record_limit scan_limit
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, opts = {})
21
+ def initialize(client, table, key_conditions, non_key_conditions, options)
21
22
  @client = client
22
23
  @table = table
23
24
 
24
- opts = opts.symbolize_keys
25
- @options = opts.slice(*OPTIONS_KEYS)
26
- @conditions = opts.except(*OPTIONS_KEYS)
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
- # Deal with various limits and batching
65
- batch_size = options[:batch_size]
66
- limit = [record_limit, scan_limit, batch_size].compact.min
67
-
68
- request[:limit] = limit if limit
69
- request[:table_name] = table.name
70
- request[:key_conditions] = key_conditions
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
- # Deal with various limits and batching
56
- batch_size = options[:batch_size]
57
- limit = [record_limit, scan_limit, batch_size].compact.min
58
-
59
- request[:limit] = limit if limit
60
- request[:table_name] = table.name
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