dynamoid 3.9.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 (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