dynamoid 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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