dynamoid 3.1.0 → 3.2.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 (47) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +18 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +113 -63
  6. data/Vagrantfile +2 -2
  7. data/docker-compose.yml +1 -1
  8. data/gemfiles/rails_4_2.gemfile +1 -1
  9. data/gemfiles/rails_5_0.gemfile +1 -1
  10. data/gemfiles/rails_5_1.gemfile +1 -1
  11. data/gemfiles/rails_5_2.gemfile +1 -1
  12. data/lib/dynamoid/adapter.rb +1 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
  17. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
  18. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
  19. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
  20. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
  21. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
  24. data/lib/dynamoid/associations/has_many.rb +1 -0
  25. data/lib/dynamoid/associations/has_one.rb +1 -0
  26. data/lib/dynamoid/associations/single_association.rb +1 -0
  27. data/lib/dynamoid/criteria.rb +4 -4
  28. data/lib/dynamoid/criteria/chain.rb +86 -79
  29. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
  30. data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
  31. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
  32. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
  33. data/lib/dynamoid/document.rb +18 -13
  34. data/lib/dynamoid/dumping.rb +52 -40
  35. data/lib/dynamoid/fields.rb +4 -3
  36. data/lib/dynamoid/finders.rb +3 -3
  37. data/lib/dynamoid/persistence.rb +5 -6
  38. data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
  39. data/lib/dynamoid/tasks.rb +1 -0
  40. data/lib/dynamoid/tasks/database.rake +2 -2
  41. data/lib/dynamoid/type_casting.rb +37 -19
  42. data/lib/dynamoid/undumping.rb +53 -42
  43. data/lib/dynamoid/validations.rb +2 -0
  44. data/lib/dynamoid/version.rb +1 -1
  45. metadata +17 -5
  46. data/lib/dynamoid/adapter_plugin/query.rb +0 -144
  47. data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middleware/backoff'
4
+ require_relative 'middleware/limit'
5
+ require_relative 'middleware/start_key'
6
+
7
+ module Dynamoid
8
+ module AdapterPlugin
9
+ class AwsSdkV3
10
+ class Scan
11
+ attr_reader :client, :table, :conditions, :options
12
+
13
+ def initialize(client, table, conditions = {}, options = {})
14
+ @client = client
15
+ @table = table
16
+ @conditions = conditions
17
+ @options = options
18
+ end
19
+
20
+ def call
21
+ request = build_request
22
+
23
+ Enumerator.new do |yielder|
24
+ api_call = -> (request) do
25
+ client.scan(request).tap do |response|
26
+ yielder << response
27
+ end
28
+ end
29
+
30
+ middlewares = Middleware::Backoff.new(
31
+ Middleware::StartKey.new(
32
+ Middleware::Limit.new(api_call, record_limit: record_limit, scan_limit: scan_limit)
33
+ )
34
+ )
35
+
36
+ catch :stop_pagination do
37
+ loop do
38
+ middlewares.call(request)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def build_request
47
+ request = options.slice(
48
+ :consistent_read,
49
+ :exclusive_start_key,
50
+ :select
51
+ ).compact
52
+
53
+ # Deal with various limits and batching
54
+ batch_size = options[:batch_size]
55
+ limit = [record_limit, scan_limit, batch_size].compact.min
56
+
57
+ request[:limit] = limit if limit
58
+ request[:table_name] = table.name
59
+ request[:scan_filter] = scan_filter
60
+
61
+ request
62
+ end
63
+
64
+ def record_limit
65
+ options[:record_limit]
66
+ end
67
+
68
+ def scan_limit
69
+ options[:scan_limit]
70
+ end
71
+
72
+ def scan_filter
73
+ conditions.reduce({}) do |result, (attr, cond)|
74
+ condition = {
75
+ comparison_operator: AwsSdkV3::FIELD_MAP[cond.keys[0]],
76
+ attribute_value_list: AwsSdkV3.attribute_value_list(AwsSdkV3::FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
77
+ }
78
+ result[attr] = condition
79
+ result
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ # Represents a table. Exposes data from the "DescribeTable" API call, and also
7
+ # provides methods for coercing values to the proper types based on the table's schema data
8
+ class Table
9
+ attr_reader :schema
10
+
11
+ #
12
+ # @param [Hash] schema Data returns from a "DescribeTable" call
13
+ #
14
+ def initialize(schema)
15
+ @schema = schema[:table]
16
+ end
17
+
18
+ def range_key
19
+ @range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
20
+ end
21
+
22
+ def range_type
23
+ range_type ||= schema[:attribute_definitions].find do |d|
24
+ d[:attribute_name] == range_key
25
+ end.try(:fetch, :attribute_type, nil)
26
+ end
27
+
28
+ def hash_key
29
+ @hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
30
+ end
31
+
32
+ #
33
+ # Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
34
+ # nil otherwise
35
+ #
36
+ def col_type(col)
37
+ col = col.to_s
38
+ col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
39
+ col_def && col_def[:attribute_type]
40
+ end
41
+
42
+ def item_count
43
+ schema[:item_count]
44
+ end
45
+
46
+ def name
47
+ schema[:table_name]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module AdapterPlugin
5
+ class AwsSdkV3
6
+ class UntilPastTableStatus
7
+ attr_reader :table_name, :status
8
+
9
+ def initialize(table_name, status = :creating)
10
+ @table_name = table_name
11
+ @status = status
12
+ end
13
+
14
+ def call
15
+ counter = 0
16
+ resp = nil
17
+ begin
18
+ check = { again: true }
19
+ while check[:again]
20
+ sleep Dynamoid::Config.sync_retry_wait_seconds
21
+ resp = client.describe_table(table_name: table_name)
22
+ check = check_table_status?(counter, resp, status)
23
+ Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
24
+ counter += 1
25
+ end
26
+ # If you issue a DescribeTable request immediately after a CreateTable
27
+ # request, DynamoDB might return a ResourceNotFoundException.
28
+ # This is because DescribeTable uses an eventually consistent query,
29
+ # and the metadata for your table might not be available at that moment.
30
+ # Wait for a few seconds, and then try the DescribeTable request again.
31
+ # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
32
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
33
+ case status
34
+ when :creating then
35
+ if counter >= Dynamoid::Config.sync_retry_max_times
36
+ Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})"
37
+ retry # start over at first line of begin, does not reset counter
38
+ else
39
+ Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})"
40
+ raise e
41
+ end
42
+ else
43
+ # When deleting a table, "not found" is the goal.
44
+ Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})"
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def check_table_status?(counter, resp, expect_status)
52
+ status = PARSE_TABLE_STATUS.call(resp)
53
+ again = counter < Dynamoid::Config.sync_retry_max_times &&
54
+ status == TABLE_STATUSES[expect_status]
55
+ { again: again, status: status, counter: counter }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -16,6 +16,7 @@ module Dynamoid #:nodoc:
16
16
  key_name = options[:inverse_of] || source.class.to_s.pluralize.underscore.to_sym
17
17
  guess = target_class.associations[key_name]
18
18
  return nil if guess.nil? || guess[:type] != :has_and_belongs_to_many
19
+
19
20
  key_name
20
21
  end
21
22
  end
@@ -16,6 +16,7 @@ module Dynamoid #:nodoc:
16
16
  key_name = options[:inverse_of] || source.class.to_s.singularize.underscore.to_sym
17
17
  guess = target_class.associations[key_name]
18
18
  return nil if guess.nil? || guess[:type] != :belongs_to
19
+
19
20
  key_name
20
21
  end
21
22
  end
@@ -17,6 +17,7 @@ module Dynamoid #:nodoc:
17
17
  key_name = options[:inverse_of] || source.class.to_s.singularize.underscore.to_sym
18
18
  guess = target_class.associations[key_name]
19
19
  return nil if guess.nil? || guess[:type] != :belongs_to
20
+
20
21
  key_name
21
22
  end
22
23
  end
@@ -81,6 +81,7 @@ module Dynamoid #:nodoc:
81
81
  # @since 0.2.0
82
82
  def find_target
83
83
  return if source_ids.empty?
84
+
84
85
  target_class.find(source_ids.first)
85
86
  end
86
87
 
@@ -8,17 +8,17 @@ module Dynamoid
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  module ClassMethods
11
- %i[where all first last each record_limit scan_limit batch start scan_index_forward].each do |meth|
11
+ %i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages].each do |meth|
12
12
  # Return a criteria chain in response to a method that will begin or end a chain. For more information,
13
13
  # see Dynamoid::Criteria::Chain.
14
14
  #
15
15
  # @since 0.2.0
16
- define_method(meth) do |*args|
16
+ define_method(meth) do |*args, &blk|
17
17
  chain = Dynamoid::Criteria::Chain.new(self)
18
18
  if args
19
- chain.send(meth, *args)
19
+ chain.send(meth, *args, &blk)
20
20
  else
21
- chain.send(meth)
21
+ chain.send(meth, &blk)
22
22
  end
23
23
  end
24
24
  end
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ require_relative 'key_fields_detector'
4
+ require_relative 'ignored_conditions_detector'
5
+ require_relative 'overwritten_conditions_detector'
6
+ require_relative 'nonexistent_fields_detector'
7
+
8
+ module Dynamoid
4
9
  module Criteria
5
10
  # The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
6
11
  # chain to relation). It is a chainable object that builds up a query and eventually executes it by a Query or Scan.
7
12
  class Chain
8
- attr_accessor :query, :source, :values, :consistent_read
9
- attr_reader :hash_key, :range_key, :index_name
13
+ attr_reader :query, :source, :consistent_read, :key_fields_detector
14
+
10
15
  include Enumerable
11
16
  # Create a new criteria chain.
12
17
  #
@@ -22,6 +27,9 @@ module Dynamoid #:nodoc:
22
27
  if @source.attributes.key?(type)
23
28
  @query[:"#{type}.in"] = @source.deep_subclasses.map(&:name) << @source.name
24
29
  end
30
+
31
+ # we should re-initialize keys detector every time we change query
32
+ @key_fields_detector = KeyFieldsDetector.new(@query, @source)
25
33
  end
26
34
 
27
35
  # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
@@ -36,7 +44,26 @@ module Dynamoid #:nodoc:
36
44
  #
37
45
  # @since 0.2.0
38
46
  def where(args)
39
- query.update(args.dup.symbolize_keys)
47
+ detector = IgnoredConditionsDetector.new(args)
48
+ if detector.found?
49
+ Dynamoid.logger.warn(detector.warning_message)
50
+ end
51
+
52
+ detector = OverwrittenConditionsDetector.new(@query, args)
53
+ if detector.found?
54
+ Dynamoid.logger.warn(detector.warning_message)
55
+ end
56
+
57
+ detector = NonexistentFieldsDetector.new(args, @source)
58
+ if detector.found?
59
+ Dynamoid.logger.warn(detector.warning_message)
60
+ end
61
+
62
+ query.update(args.symbolize_keys)
63
+
64
+ # we should re-initialize keys detector every time we change query
65
+ @key_fields_detector = KeyFieldsDetector.new(@query, @source)
66
+
40
67
  self
41
68
  end
42
69
 
@@ -53,7 +80,7 @@ module Dynamoid #:nodoc:
53
80
  end
54
81
 
55
82
  def count
56
- if key_present?
83
+ if @key_fields_detector.key_present?
57
84
  count_via_query
58
85
  else
59
86
  count_via_scan
@@ -74,13 +101,13 @@ module Dynamoid #:nodoc:
74
101
  ids = []
75
102
  ranges = []
76
103
 
77
- if key_present?
78
- Dynamoid.adapter.query(source.table_name, range_query).collect do |hash|
104
+ if @key_fields_detector.key_present?
105
+ Dynamoid.adapter.query(source.table_name, range_query).flat_map{ |i| i }.collect do |hash|
79
106
  ids << hash[source.hash_key.to_sym]
80
107
  ranges << hash[source.range_key.to_sym] if source.range_key
81
108
  end
82
109
  else
83
- Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).collect do |hash|
110
+ Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).flat_map{ |i| i }.collect do |hash|
84
111
  ids << hash[source.hash_key.to_sym]
85
112
  ranges << hash[source.range_key.to_sym] if source.range_key
86
113
  end
@@ -128,6 +155,10 @@ module Dynamoid #:nodoc:
128
155
  records.each(&block)
129
156
  end
130
157
 
158
+ def find_by_pages(&block)
159
+ pages.each(&block)
160
+ end
161
+
131
162
  private
132
163
 
133
164
  # The actual records referenced by the association.
@@ -136,42 +167,57 @@ module Dynamoid #:nodoc:
136
167
  #
137
168
  # @since 0.2.0
138
169
  def records
139
- if key_present?
140
- records_via_query
170
+ pages.lazy.flat_map { |i| i }
171
+ end
172
+
173
+ # Arrays of records, sized based on the actual pages produced by DynamoDB
174
+ #
175
+ # @return [Enumerator] an iterator of the found records.
176
+ #
177
+ # @since 3.1.0
178
+ def pages
179
+ if @key_fields_detector.key_present?
180
+ pages_via_query
141
181
  else
142
- records_via_scan
182
+ issue_scan_warning if Dynamoid::Config.warn_on_scan && query.present?
183
+ pages_via_scan
143
184
  end
144
185
  end
145
186
 
146
- def records_via_query
187
+ # If the query matches an index, we'll query the associated table to find results.
188
+ #
189
+ # @return [Enumerator] an iterator of the found pages. An array of records
190
+ #
191
+ # @since 3.1.0
192
+ def pages_via_query
147
193
  Enumerator.new do |yielder|
148
- Dynamoid.adapter.query(source.table_name, range_query).each do |hash|
149
- yielder.yield source.from_database(hash)
194
+ Dynamoid.adapter.query(source.table_name, range_query).each do |items, metadata|
195
+ yielder.yield items.map { |hash| source.from_database(hash) }, metadata.slice(:last_evaluated_key)
150
196
  end
151
197
  end
152
198
  end
153
199
 
154
200
  # If the query does not match an index, we'll manually scan the associated table to find results.
155
201
  #
156
- # @return [Enumerator] an iterator of the found records.
202
+ # @return [Enumerator] an iterator of the found pages. An array of records
157
203
  #
158
- # @since 0.2.0
159
- def records_via_scan
160
- if Dynamoid::Config.warn_on_scan && query.present?
161
- Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
162
- Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:"
163
- Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
164
- Dynamoid.logger.warn "* local_secondary_indexe range_key: 'some-name'"
165
- Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect { |name| ":#{name}" }.join(', ')}"
166
- end
167
-
204
+ # @since 3.1.0
205
+ def pages_via_scan
168
206
  Enumerator.new do |yielder|
169
- Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |hash|
170
- yielder.yield source.from_database(hash)
207
+ Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |items, metadata|
208
+ yielder.yield(items.map { |hash| source.from_database(hash) }, metadata.slice(:last_evaluated_key))
171
209
  end
172
210
  end
173
211
  end
174
212
 
213
+ def issue_scan_warning
214
+ Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
215
+ Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:"
216
+ Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
217
+ Dynamoid.logger.warn "* local_secondary_index range_key: 'some-name'"
218
+ Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect { |name| ":#{name}" }.join(', ')}"
219
+ end
220
+
175
221
  def count_via_query
176
222
  Dynamoid.adapter.query_count(source.table_name, range_query)
177
223
  end
@@ -238,24 +284,24 @@ module Dynamoid #:nodoc:
238
284
  opts = {}
239
285
 
240
286
  # Add hash key
241
- opts[:hash_key] = @hash_key
242
- opts[:hash_value] = type_cast_condition_parameter(@hash_key, query[@hash_key])
287
+ opts[:hash_key] = @key_fields_detector.hash_key
288
+ opts[:hash_value] = type_cast_condition_parameter(@key_fields_detector.hash_key, query[@key_fields_detector.hash_key])
243
289
 
244
290
  # Add range key
245
- if @range_key
246
- opts[:range_key] = @range_key
247
- if query[@range_key].present?
248
- value = type_cast_condition_parameter(@range_key, query[@range_key])
291
+ if @key_fields_detector.range_key
292
+ opts[:range_key] = @key_fields_detector.range_key
293
+ if query[@key_fields_detector.range_key].present?
294
+ value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
249
295
  opts.update(range_eq: value)
250
296
  end
251
297
 
252
- query.keys.select { |k| k.to_s =~ /^#{@range_key}\./ }.each do |key|
298
+ query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
253
299
  opts.merge!(range_hash(key))
254
300
  end
255
301
  end
256
302
 
257
- (query.keys.map(&:to_sym) - [@hash_key.to_sym, @range_key.try(:to_sym)])
258
- .reject { |k, _| k.to_s =~ /^#{@range_key}\./ }
303
+ (query.keys.map(&:to_sym) - [@key_fields_detector.hash_key.to_sym, @key_fields_detector.range_key.try(:to_sym)])
304
+ .reject { |k, _| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }
259
305
  .each do |key|
260
306
  if key.to_s.include?('.')
261
307
  opts.update(field_hash(key))
@@ -284,53 +330,14 @@ module Dynamoid #:nodoc:
284
330
  end
285
331
  end
286
332
 
287
- def key_present?
288
- query_keys = query.keys.collect { |k| k.to_s.split('.').first }
289
-
290
- # See if querying based on table hash key
291
- if query.keys.map(&:to_s).include?(source.hash_key.to_s)
292
- @hash_key = source.hash_key
293
-
294
- # Use table's default range key
295
- if query_keys.include?(source.range_key.to_s)
296
- @range_key = source.range_key
297
- return true
298
- end
299
-
300
- # See if can use any local secondary index range key
301
- # Chooses the first LSI found that can be utilized for the query
302
- source.local_secondary_indexes.each do |_, lsi|
303
- next unless query_keys.include?(lsi.range_key.to_s)
304
- @range_key = lsi.range_key
305
- @index_name = lsi.name
306
- end
307
-
308
- return true
309
- end
310
-
311
- # See if can use any global secondary index
312
- # Chooses the first GSI found that can be utilized for the query
313
- # But only do so if projects ALL attributes otherwise we won't
314
- # get back full data
315
- source.global_secondary_indexes.each do |_, gsi|
316
- next unless query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
317
- @hash_key = gsi.hash_key
318
- @range_key = gsi.range_key
319
- @index_name = gsi.name
320
- return true
321
- end
322
-
323
- # Could not utilize any indices so we'll have to scan
324
- false
325
- end
326
-
327
333
  # Start key needs to be set up based on the index utilized
328
334
  # If using a secondary index then we must include the index's composite key
329
335
  # as well as the tables composite key.
330
336
  def start_key
331
337
  return @start if @start.is_a?(Hash)
332
- hash_key = @hash_key || source.hash_key
333
- range_key = @range_key || source.range_key
338
+
339
+ hash_key = @key_fields_detector.hash_key || source.hash_key
340
+ range_key = @key_fields_detector.range_key || source.range_key
334
341
 
335
342
  key = {}
336
343
  key[hash_key] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
@@ -349,7 +356,7 @@ module Dynamoid #:nodoc:
349
356
 
350
357
  def query_opts
351
358
  opts = {}
352
- opts[:index_name] = @index_name if @index_name
359
+ opts[:index_name] = @key_fields_detector.index_name if @key_fields_detector.index_name
353
360
  opts[:select] = 'ALL_ATTRIBUTES'
354
361
  opts[:record_limit] = @record_limit if @record_limit
355
362
  opts[:scan_limit] = @scan_limit if @scan_limit