dynamoid 3.9.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -7
  3. data/README.md +20 -23
  4. data/dynamoid.gemspec +1 -2
  5. data/lib/dynamoid/adapter.rb +18 -12
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +78 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +19 -1
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +38 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +33 -27
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +87 -62
  13. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  14. data/lib/dynamoid/associations.rb +1 -1
  15. data/lib/dynamoid/config/options.rb +12 -12
  16. data/lib/dynamoid/config.rb +1 -0
  17. data/lib/dynamoid/criteria/chain.rb +95 -133
  18. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  19. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  20. data/lib/dynamoid/criteria/where_conditions.rb +29 -0
  21. data/lib/dynamoid/dirty.rb +1 -1
  22. data/lib/dynamoid/document.rb +1 -1
  23. data/lib/dynamoid/dumping.rb +2 -2
  24. data/lib/dynamoid/fields/declare.rb +6 -6
  25. data/lib/dynamoid/fields.rb +6 -8
  26. data/lib/dynamoid/finders.rb +17 -26
  27. data/lib/dynamoid/indexes.rb +6 -7
  28. data/lib/dynamoid/loadable.rb +2 -2
  29. data/lib/dynamoid/persistence/save.rb +12 -16
  30. data/lib/dynamoid/persistence/update_fields.rb +2 -2
  31. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  32. data/lib/dynamoid/persistence.rb +39 -4
  33. data/lib/dynamoid/type_casting.rb +15 -14
  34. data/lib/dynamoid/undumping.rb +1 -1
  35. data/lib/dynamoid/version.rb +1 -1
  36. metadata +17 -16
  37. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  38. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aedb91a28972fd9357cbb260341e617c3d0d35ae05a620d5ac8312f5d3edbf96
4
- data.tar.gz: a026d8c3d1e53f4e6800dcffe2b8350da50f70d953a19ae86dfeff0f65555c8c
3
+ metadata.gz: c2cc1feec6853b756fff467fa687cf2eaa3a2dc5f0a88dac948e5a92a4aa1073
4
+ data.tar.gz: 7165e081fa9979c45e7402265138e62cb6ee3c1f6f594ab6949c1f984621bc89
5
5
  SHA512:
6
- metadata.gz: dcb483ea21eaa16246d82603d18952a70bab15556b65ea94fad3ac9dbfe6358f00730e1825275d241a18675fdc0a2e4c3c4b0553acb41f9205e5fe350337f54c
7
- data.tar.gz: 4e4c96ed9b90ccab85f2f09ad848455affa681c93740dcf1f7b6283797f8e47bebd1c83a70a524eb6ea44906bdd6baec07549f67a9e6bd6561b857ea8997b692
6
+ metadata.gz: e18700ff74b9466e1ec166706446a1dca5248c6b8c33365e469c74e9a67b92f4afc9cacb0a06c2698d5165f4b68a93cdd214b83f5e3b749b92bcaff06fbc7628
7
+ data.tar.gz: 9b18fb2522f90eb3cf9b0b6014ca24beb08bcdebf403c57487222c46ec71d306f4e85e686aa89607c77ad7d994359a934537e8d22202b145bd478c0862bf497b
data/CHANGELOG.md CHANGED
@@ -7,14 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Fixed
10
-
11
10
  ### Added
12
-
13
11
  ### Changed
14
-
15
12
  ### Removed
16
13
 
17
- ## 3.9.0
14
+ ## 3.10.0
15
+ ### Fixed
16
+ * [#681](https://github.com/Dynamoid/dynamoid/pull/681) Fixed saving persisted model and deleting attributes with `nil` value if `config.store_attribute_with_nil_value` is `false`
17
+ * [#716](https://github.com/Dynamoid/dynamoid/pull/716), [#691](https://github.com/Dynamoid/dynamoid/pull/691), [#687](https://github.com/Dynamoid/dynamoid/pull/687), [#660](https://github.com/Dynamoid/dynamoid/pull/660) Numerous fixes in README.md and RDoc documentation (@ndjndj, @kiharito, @dunkOnIT)
18
+ ### Added
19
+ * [#656](https://github.com/Dynamoid/dynamoid/pull/656) Added a `create_table_on_save` configuration flag to create table on save (@imaximix)
20
+ * [#697](https://github.com/Dynamoid/dynamoid/pull/697) Ensure Ruby 3.3 and Rails 7.1 versions are supported and added them on CI
21
+ ### Changed
22
+ * [#655](https://github.com/Dynamoid/dynamoid/pull/655) Support multiple `where` in the same chain with multiple conditions for the same field
23
+
24
+ ## 3.9.0 / 2023-04-13
18
25
  ### Fixed
19
26
  * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Specs in JRuby; Support for JRuby 9.4.0.0 (@pboling)
20
27
  * [#624](https://github.com/Dynamoid/dynamoid/pull/624) Fixed `#increment!`/`#decrement!` methods and made them compatible with Rails counterparts
@@ -50,15 +57,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
57
  * `#touch`
51
58
  * `#increment!`
52
59
  * `#decrement!`
53
- * [#642](https://github.com/Dynamoid/dynamoid/pull/642) Run specs on CI agains Ruby 3.2
60
+ * [#642](https://github.com/Dynamoid/dynamoid/pull/642) Run specs on CI against Ruby 3.2
54
61
  * [#645](https://github.com/Dynamoid/dynamoid/pull/645) Added `after_find` callback
55
62
  ### Changed
56
63
  * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Switch to [`rubocop-lts`](https://rubocop-lts.gitlab.io/) (@pboling)
57
- ### Removed
58
64
  * [#633](https://github.com/Dynamoid/dynamoid/pull/633) Change `#inspect` method to return only attributes
59
65
  * [#623](https://github.com/Dynamoid/dynamoid/pull/623) Optimized performance of persisting to send only changed attributes in a request to DynamoDB
60
66
 
61
- ## 3.8.0
67
+ ## 3.8.0 / 2022-11-09
62
68
  ### Fixed
63
69
  * [#525](https://github.com/Dynamoid/dynamoid/pull/525) Don't mark an attribute as changed if new assigned value equals the old one (@a5-stable)
64
70
  * Minor changes in the documentation:
data/README.md CHANGED
@@ -67,10 +67,10 @@ For example, to configure AWS access:
67
67
  Create `config/initializers/aws.rb` as follows:
68
68
 
69
69
  ```ruby
70
- Aws.config.update({
71
- region: 'us-west-2',
70
+ Aws.config.update(
71
+ region: 'us-west-2',
72
72
  credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'),
73
- })
73
+ )
74
74
  ```
75
75
 
76
76
  Alternatively, if you don't want Aws connection settings to be
@@ -132,8 +132,8 @@ end
132
132
  Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
133
133
 
134
134
  Its compatibility is tested against following Ruby versions: 2.3, 2.4,
135
- 2.5, 2.6, 2.7, 3.0, 3.1 and 3.2, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
136
- 5.2, 6.0, 6.1 and 7.0.
135
+ 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and 3.3, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
136
+ 5.2, 6.0, 6.1, 7.0 and 7.1.
137
137
 
138
138
  ## Setup
139
139
 
@@ -723,18 +723,6 @@ join, but instead finds all the user's addresses and naively filters
723
723
  them in Ruby. For large associations this is a performance hit compared
724
724
  to relational database engines.
725
725
 
726
- **WARNING:** There is a limitation of conditions passed to `where`
727
- method. Only one condition for some particular field could be specified.
728
- The last one only will be applied and others will be ignored. E.g. in
729
- examples:
730
-
731
- ```ruby
732
- User.where('age.gt': 10, 'age.lt': 20)
733
- User.where(name: 'Mike').where('name.begins_with': 'Ed')
734
- ```
735
-
736
- the first one will be ignored and the last one will be used.
737
-
738
726
  **Warning:** There is a caveat with filtering documents by `nil` value
739
727
  attribute. By default Dynamoid ignores attributes with `nil` value and
740
728
  doesn't store them in a DynamoDB document. This behavior could be
@@ -752,7 +740,7 @@ If Dynamoid keeps `nil` value attributes `eq`/`ne` operators should be
752
740
  used instead:
753
741
 
754
742
  ```ruby
755
- Address.where('postcode': nil)
743
+ Address.where(postcode: nil)
756
744
  Address.where('postcode.ne': nil)
757
745
  ```
758
746
 
@@ -926,8 +914,8 @@ If you have a range index, Dynamoid provides a number of additional
926
914
  other convenience methods to make your life a little easier:
927
915
 
928
916
  ```ruby
929
- User.where("created_at.gt": DateTime.now - 1.day).all
930
- User.where("created_at.lt": DateTime.now - 1.day).all
917
+ User.where('created_at.gt': DateTime.now - 1.day).all
918
+ User.where('created_at.lt': DateTime.now - 1.day).all
931
919
  ```
932
920
 
933
921
  It also supports `gte` and `lte`. Turning those into symbols and
@@ -975,7 +963,7 @@ To idempotently create-but-not-update a record, apply the `unless_exists` condit
975
963
  to its keys when you upsert.
976
964
 
977
965
  ```ruby
978
- Address.upsert(id, { city: 'Chicago' }, if: { unless_exists: [:id] })
966
+ Address.upsert(id, { city: 'Chicago' }, { unless_exists: [:id] })
979
967
  ```
980
968
 
981
969
  ### Deleting
@@ -1144,12 +1132,12 @@ Listed below are all configuration options.
1144
1132
  in ISO 8601 string format. Default is `false`
1145
1133
  * `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
1146
1134
  as native DynamoDB boolean values. Otherwise boolean fields are stored
1147
- as string values `'t'` and `'f'`. Default is true
1135
+ as string values `'t'` and `'f'`. Default is `true`
1148
1136
  * `backoff` - is a hash: key is a backoff strategy (symbol), value is
1149
1137
  parameters for the strategy. Is used in batch operations. Default id
1150
1138
  `nil`
1151
1139
  * `backoff_strategies`: is a hash and contains all available strategies.
1152
- Default is { constant: ..., exponential: ...}
1140
+ Default is `{ constant: ..., exponential: ...}`
1153
1141
  * `log_formatter`: overrides default AWS SDK formatter. There are
1154
1142
  several canned formatters: `Aws::Log::Formatter.default`,
1155
1143
  `Aws::Log::Formatter.colored` and `Aws::Log::Formatter.short`. Please
@@ -1167,6 +1155,9 @@ Listed below are all configuration options.
1167
1155
  * `http_read_timeout`:The number of seconds to wait for HTTP response
1168
1156
  data. Default option value is `nil`. If not specified effected value
1169
1157
  is `60`
1158
+ * `create_table_on_save`: if `true` then Dynamoid creates a
1159
+ corresponding table in DynamoDB at model persisting if the table
1160
+ doesn't exist yet. Default is `true`
1170
1161
 
1171
1162
 
1172
1163
  ## Concurrency
@@ -1305,6 +1296,12 @@ RSpec.configure do |config|
1305
1296
  end
1306
1297
  ```
1307
1298
 
1299
+ In addition, the first test for each model may fail if the relevant models are not included in `included_models`. This can be fixed by adding this line before the `DynamoidReset` module:
1300
+ ```ruby
1301
+ Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
1302
+ ```
1303
+ Note that this will require _all_ models in your models folder - you can also explicitly require only certain models if you would prefer to.
1304
+
1308
1305
  In Rails, you may also want to ensure you do not delete non-test data
1309
1306
  accidentally by adding the following to your test environment setup:
1310
1307
 
data/dynamoid.gemspec CHANGED
@@ -59,8 +59,7 @@ Gem::Specification.new do |spec|
59
59
  spec.add_development_dependency 'bundler'
60
60
  spec.add_development_dependency 'pry', '~> 0.14'
61
61
  spec.add_development_dependency 'rake', '~> 13.0'
62
+ spec.add_development_dependency 'rexml'
62
63
  spec.add_development_dependency 'rspec', '~> 3.12'
63
- # 'rubocop-lts' is for Ruby 2.3+, see https://rubocop-lts.gitlab.io/
64
- spec.add_development_dependency 'rubocop-lts', '~> 10.0'
65
64
  spec.add_development_dependency 'yard'
66
65
  end
@@ -171,19 +171,25 @@ module Dynamoid
171
171
  # only really useful for range queries, since it can only find by one hash key at once. Only provide
172
172
  # one range key to the hash.
173
173
  #
174
+ # Dynamoid.adapter.query('users', { id: [[:eq, '1']], age: [[:between, [10, 30]]] }, { batch_size: 1000 })
175
+ #
174
176
  # @param [String] table_name the name of the table
175
- # @param [Hash] opts the options to query the table with
176
- # @option opts [String] :hash_value the value of the hash key to find
177
- # @option opts [Range] :range_value find the range key within this range
178
- # @option opts [Number] :range_greater_than find range keys greater than this
179
- # @option opts [Number] :range_less_than find range keys less than this
180
- # @option opts [Number] :range_gte find range keys greater than or equal to this
181
- # @option opts [Number] :range_lte find range keys less than or equal to this
182
- #
183
- # @return [Array] an array of all matching items
184
- #
185
- def query(table_name, opts = {})
186
- adapter.query(table_name, opts)
177
+ # @param [Array[Array]] key_conditions conditions for the primary key attributes
178
+ # @param [Array[Array]] non_key_conditions (optional) conditions for non-primary key attributes
179
+ # @param [Hash] options (optional) the options to query the table with
180
+ # @option options [Boolean] :consistent_read You can set the ConsistentRead parameter to true and obtain a strongly consistent result
181
+ # @option options [Boolean] :scan_index_forward Specifies the order for index traversal: If true (default), the traversal is performed in ascending order; if false, the traversal is performed in descending order.
182
+ # @option options [Symbop] :select The attributes to be returned in the result (one of ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, ...)
183
+ # @option options [Symbol] :index_name The name of an index to query. This index can be any local secondary index or global secondary index on the table.
184
+ # @option options [Hash] :exclusive_start_key The primary key of the first item that this operation will evaluate.
185
+ # @option options [Integer] :batch_size The number of items to lazily load one by one
186
+ # @option options [Integer] :record_limit The maximum number of items to return (not necessarily the number of evaluated items)
187
+ # @option options [Integer] :scan_limit The maximum number of items to evaluate (not necessarily the number of matching items)
188
+ # @option options [Array[Symbol]] :project The attributes to retrieve from the table
189
+ #
190
+ # @return [Enumerable] matching items
191
+ def query(table_name, key_conditions, non_key_conditions = {}, options = {})
192
+ adapter.query(table_name, key_conditions, non_key_conditions, options)
187
193
  end
188
194
 
189
195
  def self.adapter_plugin_class
@@ -29,7 +29,7 @@ module Dynamoid
29
29
  gs_indexes = options[:global_secondary_indexes]
30
30
 
31
31
  key_schema = {
32
- hash_key_schema: { key => (options[:hash_key_type] || :string) },
32
+ hash_key_schema: { key => options[:hash_key_type] || :string },
33
33
  range_key_schema: options[:range_key]
34
34
  }
35
35
  attribute_definitions = build_all_attribute_definitions(
@@ -69,7 +69,7 @@ module Dynamoid
69
69
  end
70
70
  end
71
71
  resp = client.create_table(client_opts)
72
- options[:sync] = true if !options.key?(:sync) && ls_indexes.present? || gs_indexes.present?
72
+ options[:sync] = true if (!options.key?(:sync) && ls_indexes.present?) || gs_indexes.present?
73
73
 
74
74
  if options[:sync]
75
75
  status = PARSE_TABLE_STATUS.call(resp, :table_description)
@@ -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,7 +97,12 @@ 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
107
  if v.is_a?(Hash)
91
108
  v.stringify_keys
@@ -97,6 +114,7 @@ module Dynamoid
97
114
  v
98
115
  end
99
116
  end
117
+ # rubocop:enable Lint/DuplicateBranch
100
118
  end
101
119
  end
102
120
  end
@@ -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