fake_dynamo 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aa98039cb0185d49b68cf2c5d7f4fc2e397b46d5
4
+ data.tar.gz: 46c7912a403e2a286845ac5adeed6222f165581b
5
+ SHA512:
6
+ metadata.gz: 6e44b53a1d8bfc7635386a0723bac32b24e09bc796a480499934aec4bc72b3682b52946d3ebc76d8f0fdcc1b4b330b841c1d2250c213f8fb8f70020f5f0c8da3
7
+ data.tar.gz: c71684a8780aa64b655b33dcca221979114e7986940d12016d3955fed1075c337d567bd3206dc72eafbd91e72b98064ca17b3bbfebd0ed5b6de9f829168abc13
data/.gitignore CHANGED
@@ -15,3 +15,7 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ bin/node_modules/*
19
+ bin/*.json
20
+ bin/reverse_dynamo
21
+ bin/test_binary
data/.travis.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ - 2.0.0
4
5
  - jruby-19mode
5
6
  - rbx-19mode
6
7
  script: "bundle exec rspec"
data/README.md CHANGED
@@ -6,16 +6,13 @@ local hosted, inmemory Amazon DynamoDB emulator.
6
6
 
7
7
  | Amazon DynamoDB | FakeDynamo |
8
8
  | --------------- | ----------- |
9
- | 2012-08-10 | 0.2.1 |
9
+ | 2012-08-10 | 0.2.2 |
10
10
  | 2011-12-05 | 0.1.3 |
11
11
 
12
12
 
13
13
  ## Caveats
14
14
 
15
15
  * `ConsumedCapacityUnits` value will be 1 always.
16
- * The response size is not constrained by 1mb limit. So operation
17
- like `BatchGetItem` will return all items irrespective of the
18
- response size
19
16
 
20
17
  ## Usage
21
18
 
@@ -35,7 +32,7 @@ curl -X DELETE http://localhost:4567
35
32
 
36
33
  ## Clients
37
34
 
38
- * aws-sdk
35
+ * aws-sdk-ruby (AWS SDK for Ruby)
39
36
 
40
37
  ````ruby
41
38
  AWS.config(:use_ssl => false,
@@ -45,6 +42,17 @@ AWS.config(:use_ssl => false,
45
42
  :secret_access_key => "xxx")
46
43
  ````
47
44
 
45
+ * aws-sdk-js (AWS SDK for Node.js)
46
+
47
+ ````js
48
+ AWS.config.update({apiVersion: "2012-08-10",
49
+ sslEnabled: false,
50
+ endpoint: "localhost:4567",
51
+ accessKeyId: "xxx",
52
+ secretAccessKey: "xxx",
53
+ region: "xxx"});
54
+ ````
55
+
48
56
  __please open a pull request with your configuration if you are using
49
57
  fake_dynamo with clients other than the ones mentioned above__.
50
58
 
data/bin/fake_dynamo CHANGED
@@ -4,7 +4,8 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
4
  require 'fake_dynamo'
5
5
  require 'optparse'
6
6
 
7
- options = { :port => 4567, :bind => '127.0.0.1', :compact => false, :db => '/usr/local/var/fake_dynamo/db.fdb' }
7
+ options = { :port => 4567, :bind => '127.0.0.1', :compact => false,
8
+ :db => '/usr/local/var/fake_dynamo/db.fdb', :log_level => :warn }
8
9
  OptionParser.new do |opts|
9
10
  opts.banner = "Usage: fake_dynamo [options]"
10
11
 
@@ -32,6 +33,10 @@ OptionParser.new do |opts|
32
33
  options[:daemonize] = daemonize
33
34
  end
34
35
 
36
+ opts.on "-l", "--log-level LEVEL", [:debug, :info, :warn, :error, :fatal], "(debug, info, warn, error, fatal) Default: #{options[:log_level]}" do |level|
37
+ options[:log_level] = level
38
+ end
39
+
35
40
  end.parse!
36
41
 
37
42
  if options[:daemonize]
@@ -53,6 +58,7 @@ if (pid = options[:pid])
53
58
  end
54
59
 
55
60
  FakeDynamo::Storage.db_path = options[:db]
61
+ FakeDynamo::Logger.setup(options[:log_level])
56
62
 
57
63
  if options[:compact]
58
64
  FakeDynamo::Storage.instance.load_aof
@@ -60,7 +66,9 @@ if options[:compact]
60
66
  end
61
67
 
62
68
  FakeDynamo::Storage.instance.load_aof
63
- FakeDynamo::Server.run!(:port => options[:port], :bind => options[:bind])
69
+ FakeDynamo::Server.run!(:port => options[:port], :bind => options[:bind]) do |server|
70
+ server.config[:AccessLog] = []
71
+ end
64
72
 
65
73
  at_exit {
66
74
  FakeDynamo::Storage.instance.shutdown
data/lib/fake_dynamo.rb CHANGED
@@ -3,7 +3,10 @@ require 'json'
3
3
  require 'base64'
4
4
  require 'active_support/inflector'
5
5
  require 'active_support/core_ext/class/attribute'
6
+ require 'pp'
7
+ require 'logger'
6
8
  require 'fake_dynamo/exceptions'
9
+ require 'fake_dynamo/num'
7
10
  require 'fake_dynamo/validation'
8
11
  require 'fake_dynamo/filter'
9
12
  require 'fake_dynamo/local_secondary_index'
@@ -12,9 +15,9 @@ require 'fake_dynamo/attribute'
12
15
  require 'fake_dynamo/key_schema'
13
16
  require 'fake_dynamo/item'
14
17
  require 'fake_dynamo/key'
18
+ require 'fake_dynamo/sack'
15
19
  require 'fake_dynamo/table'
16
20
  require 'fake_dynamo/db'
17
21
  require 'fake_dynamo/storage'
18
22
  require 'fake_dynamo/server'
19
- require 'pp'
20
-
23
+ require 'fake_dynamo/logger'
@@ -1222,6 +1222,12 @@
1222
1222
  ReturnConsumedCapacity:
1223
1223
  - :string
1224
1224
  - :enum: [TOTAL, NONE]
1225
+ TotalSegments:
1226
+ - :integer
1227
+ - :within: !ruby/range 1..4096
1228
+ Segment:
1229
+ - :integer
1230
+ - :within: !ruby/range 0..4095
1225
1231
  :outputs:
1226
1232
  Items:
1227
1233
  :sym: :member
@@ -3,44 +3,31 @@ module FakeDynamo
3
3
  attr_accessor :name, :value, :type
4
4
 
5
5
  def initialize(name, value, type)
6
- @name, @value, @type = name, value, type
6
+ @name, @type = name, type
7
+ validate_name!
8
+ return unless value
7
9
 
8
- if @type == 'B' and value
9
- @value = Base64.decode64(value)
10
- end
10
+ @value = decode(value)
11
+ validate_value!
12
+ end
11
13
 
12
- if @type == 'BS'
13
- @value = value.map { |v| Base64.decode64(v) }
14
+ def validate_name!
15
+ if name == ''
16
+ raise ValidationException, 'Empty attribute name'
14
17
  end
18
+ end
15
19
 
20
+ def validate_value!
16
21
  if ['NS', 'SS', 'BS'].include? @type
17
- raise ValidationException, 'An AttributeValue may not contain an empty set' if value.empty?
22
+ raise ValidationException, 'An AttributeValue may not contain an empty set' if @value.empty?
18
23
  raise ValidationException, 'Input collection contains duplicates' if value.uniq!
19
24
  end
20
25
 
21
- if ['NS', 'N'].include? @type
22
- Array(@value).each do |n|
23
- numeric(n)
24
- end
25
- end
26
-
27
26
  if ['S', 'SS', 'S', 'BS'].include? @type
28
- Array(value).each do |v|
27
+ Array(@value).each do |v|
29
28
  raise ValidationException, 'An AttributeValue may not contain an empty string or empty binary' if v == ''
30
29
  end
31
30
  end
32
-
33
- if name == ''
34
- raise ValidationException, 'Empty attribute name'
35
- end
36
- end
37
-
38
- def numeric(n)
39
- begin
40
- Float(n)
41
- rescue
42
- raise ValidationException, "The parameter cannot be converted to a numeric value: #{n}"
43
- end
44
31
  end
45
32
 
46
33
  def description
@@ -50,30 +37,38 @@ module FakeDynamo
50
37
  }
51
38
  end
52
39
 
40
+ def decode(value)
41
+ case @type
42
+ when 'B' then Base64.decode64(value)
43
+ when 'BS' then value.map { |v| Base64.decode64(v) }
44
+ when 'N' then Num.new(value)
45
+ when 'NS' then value.map { |v| Num.new(v) }
46
+ else value
47
+ end
48
+ end
49
+
50
+ def encode(value)
51
+ case @type
52
+ when 'B' then Base64.encode64(value)
53
+ when 'BS' then value.map { |v| Base64.encode64(v) }
54
+ when 'N' then value.to_s
55
+ when 'NS' then value.map(&:to_s)
56
+ else value
57
+ end
58
+ end
59
+
53
60
  def as_hash
54
- value = if @type == 'B'
55
- Base64.encode64(@value)
56
- elsif @type == 'BS'
57
- @value.map { |v| Base64.encode64(v) }
58
- else
59
- @value
60
- end
61
-
62
- { @name => { @type => value } }
61
+ { @name => { @type => encode(@value) } }
63
62
  end
64
63
 
65
64
  def ==(attribute)
66
65
  @name == attribute.name &&
67
66
  @type == attribute.type &&
68
- (@type == 'N' ? @value.to_f == attribute.value.to_f : @value == attribute.value)
67
+ @value == attribute.value
69
68
  end
70
69
 
71
70
  def <=>(other)
72
- if @type == 'N'
73
- @value.to_f <=> other.value.to_f
74
- else
75
- @value <=> other.value
76
- end
71
+ @value <=> other.value
77
72
  end
78
73
 
79
74
  def eql?(attribute)
@@ -83,7 +78,7 @@ module FakeDynamo
83
78
  end
84
79
 
85
80
  def hash
86
- name.hash ^ (type == 'N' ? value.to_f : value).hash ^ type.hash
81
+ name.hash ^ value.hash ^ type.hash
87
82
  end
88
83
 
89
84
  class << self
@@ -91,6 +91,8 @@ module FakeDynamo
91
91
  def batch_get_item(data)
92
92
  response = {}
93
93
  consumed_capacity = {}
94
+ unprocessed_keys = {}
95
+ sack = Sack.new
94
96
 
95
97
  data['RequestItems'].each do |table_name, table_data|
96
98
  table = find_table(table_name)
@@ -101,13 +103,23 @@ module FakeDynamo
101
103
  end
102
104
 
103
105
  table_data['Keys'].each do |key|
104
- if item_hash = table.get_raw_item(key, table_data['AttributesToGet'])
105
- response[table_name] << item_hash
106
+ if sack.has_space?
107
+ if item_hash = table.get_raw_item(key, table_data['AttributesToGet'])
108
+ response[table_name] << item_hash
109
+ sack.add(item_hash)
110
+ end
111
+ else
112
+ unless unprocessed_keys[table_name]
113
+ unprocessed_keys[table_name] = {'Keys' => []}
114
+ unprocessed_keys[table_name]['AttributesToGet'] = table_data['AttributesToGet'] if table_data['AttributesToGet']
115
+ end
116
+
117
+ unprocessed_keys[table_name]['Keys'] << key
106
118
  end
107
119
  end
108
120
  end
109
121
 
110
- response = { 'Responses' => response, 'UnprocessedKeys' => {} }
122
+ response = { 'Responses' => response, 'UnprocessedKeys' => unprocessed_keys }
111
123
  merge_consumed_capacity(consumed_capacity, response)
112
124
  end
113
125
 
@@ -195,7 +207,7 @@ module FakeDynamo
195
207
 
196
208
 
197
209
  def find_table(table_name)
198
- tables[table_name] or raise ResourceNotFoundException, "Table : #{table_name} not found"
210
+ tables[table_name] or raise ResourceNotFoundException, "Table: #{table_name} not found"
199
211
  end
200
212
 
201
213
  def check_max_request(count)
@@ -23,11 +23,7 @@ module FakeDynamo
23
23
  return false if target_attribute.type != value_attribute.type
24
24
  end
25
25
 
26
- if target_attribute.type == 'N'
27
- comparator.call(target_attribute.value.to_f, *value_attribute_list.map(&:value).map(&:to_f))
28
- else
29
- comparator.call(target_attribute.value, *value_attribute_list.map(&:value))
30
- end
26
+ comparator.call(target_attribute.value, *value_attribute_list.map(&:value))
31
27
  end
32
28
 
33
29
  def validate_supported_types(value_attribute, supported_types)
@@ -39,6 +39,10 @@ module FakeDynamo
39
39
  result
40
40
  end
41
41
 
42
+ def to_json(*)
43
+ as_hash.to_json
44
+ end
45
+
42
46
  def update(name, data)
43
47
  if key[name]
44
48
  raise ValidationException, "Cannot update attribute #{name}. This attribute is part of the key"
@@ -90,12 +94,7 @@ module FakeDynamo
90
94
  validate_type(value, old_attribute)
91
95
  case attribute.type
92
96
  when "N"
93
- new_value = (old_attribute.value.to_f + attribute.value.to_f)
94
- if new_value.truncate == new_value
95
- new_value = new_value.truncate
96
- end
97
-
98
- old_attribute.value = new_value.to_s
97
+ old_attribute.value = old_attribute.value.add(attribute.value)
99
98
  else
100
99
  old_attribute.value += attribute.value
101
100
  old_attribute.value.uniq!
@@ -0,0 +1,24 @@
1
+ module FakeDynamo
2
+ module Logger
3
+ class << self
4
+ attr_accessor :log
5
+
6
+ def setup(level)
7
+ logger = ::Logger.new(STDOUT)
8
+ logger.level = [:debug, :info, :warn, :error, :fatal].index(level)
9
+ logger.formatter = proc do |severity, datetime, progname, msg|
10
+ "#{msg}\n"
11
+ end
12
+
13
+ def logger.pp(object)
14
+ return if level > ::Logger::INFO
15
+ output = ''
16
+ PP.pp(object, output)
17
+ info(output)
18
+ end
19
+
20
+ @log = logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ require 'bigdecimal'
2
+
3
+ module FakeDynamo
4
+ class Num
5
+ include Comparable
6
+ attr_accessor :internal
7
+ LOW = BigDecimal.new('-.1e126')
8
+ HIGH = BigDecimal.new('.1e126')
9
+
10
+ def initialize(value)
11
+ validate!(value)
12
+ @internal = BigDecimal.new(value)
13
+ end
14
+
15
+ def validate!(n)
16
+ begin
17
+ Float(n)
18
+ rescue
19
+ raise ValidationException, "The parameter cannot be converted to a numeric value: #{n}"
20
+ end
21
+
22
+ b = BigDecimal.new(n)
23
+ if b < LOW
24
+ raise ValidationException, "Number underflow. Attempting to store a number with magnitude smaller than supported range"
25
+ end
26
+
27
+ if b > HIGH
28
+ raise ValidationException, "Number overflow. Attempting to store a number with magnitude larger than supported range"
29
+ end
30
+
31
+ significant = b.to_s('F').sub('.', '')
32
+ .sub(/^0*/, '').sub(/0*$/, '').size
33
+
34
+ if significant > 38
35
+ raise ValidationException, "Attempting to store more than 38 significant digits in a Number"
36
+ end
37
+ end
38
+
39
+ def <=>(other)
40
+ @internal <=> other.internal
41
+ end
42
+
43
+ def ==(other)
44
+ @internal == other.internal
45
+ end
46
+
47
+ def hash
48
+ to_s.hash
49
+ end
50
+
51
+ def add(other)
52
+ return Num.new(@internal + other.internal)
53
+ end
54
+
55
+ def eql?(other)
56
+ self == other
57
+ end
58
+
59
+ def to_s
60
+ @internal.to_s('F').sub(/\.0$/, '')
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ module FakeDynamo
2
+ class Sack
3
+ attr_accessor :size, :item, :max_size
4
+
5
+ def initialize(max_size = 1 * 1024 * 1024) # 1 mb
6
+ @size = 0
7
+ @max_size = max_size
8
+ end
9
+
10
+ def add(item)
11
+ @size += item.to_json.bytesize
12
+ end
13
+
14
+ def has_space?
15
+ @size < @max_size
16
+ end
17
+ end
18
+ end
@@ -13,16 +13,16 @@ module FakeDynamo
13
13
  begin
14
14
  data = JSON.parse(request.body.read)
15
15
  operation = extract_operation(request.env)
16
- puts "operation #{operation}"
17
- puts "data"
18
- pp data
16
+ log.info "operation #{operation}"
17
+ log.info "data"
18
+ log.pp(data)
19
19
  response = db.process(operation, data)
20
20
  storage.persist(operation, data)
21
21
  rescue FakeDynamo::Error => e
22
22
  response, status = e.response, e.status
23
23
  end
24
- puts "response"
25
- pp response
24
+ log.info "response"
25
+ log.pp(response)
26
26
  [status, response.to_json]
27
27
  end
28
28
 
@@ -40,6 +40,10 @@ module FakeDynamo
40
40
  Storage.instance
41
41
  end
42
42
 
43
+ def log
44
+ Logger.log
45
+ end
46
+
43
47
  def extract_operation(env)
44
48
  if env['HTTP_X_AMZ_TARGET'] =~ /DynamoDB_\d+\.([a-zA-z]+)/
45
49
  $1
@@ -14,6 +14,10 @@ module FakeDynamo
14
14
  end
15
15
  end
16
16
 
17
+ def log
18
+ Logger.log
19
+ end
20
+
17
21
  def initialize
18
22
  init_db
19
23
  end
@@ -42,7 +46,7 @@ module FakeDynamo
42
46
  end
43
47
 
44
48
  def reset
45
- puts "resetting database ..."
49
+ log.warn "resetting database ..."
46
50
  @aof.close if @aof
47
51
  @aof = nil
48
52
  delete_db
@@ -57,7 +61,7 @@ module FakeDynamo
57
61
  end
58
62
 
59
63
  def shutdown
60
- puts "shutting down fake_dynamo ..."
64
+ log.warn "shutting down fake_dynamo ..."
61
65
  @aof.close if @aof
62
66
  end
63
67
 
@@ -73,7 +77,7 @@ module FakeDynamo
73
77
  def load_aof
74
78
  return if @loaded
75
79
  file = File.new(db_path, 'r')
76
- puts "Loading fake_dynamo data ..."
80
+ log.warn "Loading fake_dynamo data ..."
77
81
  loop do
78
82
  operation = file.readline.chomp
79
83
  size = Integer(file.readline.chomp)
@@ -100,7 +104,7 @@ module FakeDynamo
100
104
  def compact!
101
105
  return if @compacted
102
106
  @aof = Tempfile.new('compact')
103
- puts "Compacting db ..."
107
+ log.warn "Compacting db ..."
104
108
  db.tables.each do |_, table|
105
109
  persist('CreateTable', table.create_table_data)
106
110
  table.items.each do |_, item|
@@ -212,7 +212,7 @@ module FakeDynamo
212
212
 
213
213
  def query(data)
214
214
  range_key_present
215
- select_and_attributes_to_get_present(data)
215
+ select_and_attributes_to_get_present?(data)
216
216
  validate_limit(data)
217
217
 
218
218
  index = nil
@@ -244,7 +244,7 @@ module FakeDynamo
244
244
  conditions = {}
245
245
  end
246
246
 
247
- results, last_evaluated_item, _ = filter(matched_items, conditions, data['Limit'], true)
247
+ results, last_evaluated_item, _ = filter(matched_items, conditions, data['Limit'], true, sack_attributes(data, index))
248
248
 
249
249
  response = {'Count' => results.size}.merge(consumed_capacity(data))
250
250
  merge_items(response, data, results, index)
@@ -260,10 +260,21 @@ module FakeDynamo
260
260
  end
261
261
 
262
262
  def scan(data)
263
- select_and_attributes_to_get_present(data)
263
+ select_and_attributes_to_get_present?(data)
264
+ total_segments_and_segment_present?(data)
264
265
  validate_limit(data)
266
+
265
267
  conditions = data['ScanFilter'] || {}
266
- all_items = drop_till_start(items.values, data['ExclusiveStartKey'], true, key_schema)
268
+
269
+
270
+ if (segment = data['Segment']) && (total_segments = data['TotalSegments'])
271
+ chunk_size = (items.values.size / total_segments.to_f).ceil
272
+ current_segment = items.values.slice(segment * chunk_size, chunk_size) || []
273
+ else
274
+ current_segment = items.values
275
+ end
276
+
277
+ all_items = drop_till_start(current_segment, data['ExclusiveStartKey'], true, key_schema)
267
278
  results, last_evaluated_item, scaned_count = filter(all_items, conditions, data['Limit'], false)
268
279
  response = {
269
280
  'Count' => results.size,
@@ -279,19 +290,45 @@ module FakeDynamo
279
290
  end
280
291
 
281
292
  def merge_items(response, data, results, index = nil)
293
+ if (attrs = attributes_to_get(data, index)) != false
294
+ response['Items'] = results.map { |r| filter_attributes(r, attrs) }
295
+ end
296
+ response
297
+ end
298
+
299
+ def attributes_to_get(data, index)
282
300
  if data['Select'] != 'COUNT'
283
- attributes_to_get = nil # select everything
301
+ if index
302
+ attributes_to_get = projected_attributes(index)
303
+ else
304
+ attributes_to_get = nil # select everything
305
+ end
306
+
284
307
 
285
308
  if data['AttributesToGet']
286
309
  attributes_to_get = data['AttributesToGet']
287
310
  elsif data['Select'] == 'ALL_PROJECTED_ATTRIBUTES'
288
311
  attributes_to_get = projected_attributes(index)
312
+ elsif data['Select'] == 'ALL_ATTRIBUTES'
313
+ attributes_to_get = nil
289
314
  end
315
+ else
316
+ false
317
+ end
318
+ end
319
+
320
+ def sack_attributes(data, index)
321
+ return unless index
290
322
 
291
- response['Items'] = results.map { |r| filter_attributes(r, attributes_to_get) }
323
+ if data['Select'] == 'COUNT'
324
+ return projected_attributes(index)
292
325
  end
293
326
 
294
- response
327
+ if attrs = attributes_to_get(data, index)
328
+ if (attrs - projected_attributes(index)).empty?
329
+ return projected_attributes(index)
330
+ end
331
+ end
295
332
  end
296
333
 
297
334
  def projected_attributes(index)
@@ -309,13 +346,30 @@ module FakeDynamo
309
346
  end
310
347
  end
311
348
 
312
- def select_and_attributes_to_get_present(data)
349
+ def select_and_attributes_to_get_present?(data)
313
350
  select = data['Select']
314
351
  if select and data['AttributesToGet'] and (select != 'SPECIFIC_ATTRIBUTES')
315
352
  raise ValidationException, "Cannot specify the AttributesToGet when choosing to get only the #{select}"
316
353
  end
317
354
  end
318
355
 
356
+ def total_segments_and_segment_present?(data)
357
+ segment, total_segments = data['Segment'], data['TotalSegments']
358
+
359
+ if (total_segments && !segment)
360
+ raise ValidationException, "The Segment parameter is required but was not present in the request when parameter TotalSegments is present"
361
+ end
362
+
363
+ if (segment && !total_segments)
364
+ raise ValidationException, "The TotalSegments parameter is required but was not present in the request when Segment parameter is present"
365
+ end
366
+
367
+ if (segment && total_segments) &&
368
+ (segment >= total_segments)
369
+ raise ValidationException, "The Segment parameter is zero-based and must be less than parameter TotalSegments: Segment: #{segment} is not less than TotalSegments: #{total_segments}"
370
+ end
371
+ end
372
+
319
373
  def validate_limit(data)
320
374
  if data['Limit'] and data['Limit'] <= 0
321
375
  raise ValidationException, "Limit failed to satisfy constraint: Member must have value greater than or equal to 1"
@@ -364,9 +418,10 @@ module FakeDynamo
364
418
  end
365
419
  end
366
420
 
367
- def filter(items, conditions, limit, fail_on_type_mismatch)
421
+ def filter(items, conditions, limit, fail_on_type_mismatch, sack_attributes = nil)
368
422
  limit ||= -1
369
423
  result = []
424
+ sack = Sack.new
370
425
  last_evaluated_item = nil
371
426
  scaned_count = 0
372
427
  items.each do |item|
@@ -384,7 +439,13 @@ module FakeDynamo
384
439
 
385
440
  if select
386
441
  result << item
387
- if (limit -= 1) == 0
442
+ if sack_attributes
443
+ sack.add(filter_attributes(item, sack_attributes))
444
+ else
445
+ sack.add(item)
446
+ end
447
+
448
+ if (limit -= 1) == 0 || (!sack.has_space?)
388
449
  last_evaluated_item = item
389
450
  break
390
451
  end
@@ -66,7 +66,7 @@ module FakeDynamo
66
66
  end
67
67
  when :within
68
68
  range = constrain[:within]
69
- unless range.include? data.size
69
+ unless range.include?(Numeric === data ? data : data.size)
70
70
  add_errors("The parameter '#{param(attribute, parents)}' value '#{data}' should be within #{range}")
71
71
  end
72
72
  when :enum
@@ -1,3 +1,3 @@
1
1
  module FakeDynamo
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -289,6 +289,20 @@ module FakeDynamo
289
289
  }.to raise_error(FakeDynamo::ValidationException)
290
290
  end
291
291
 
292
+ it 'should return unprocessed keys if the response is more than 1 mb' do
293
+ keys = []
294
+ request = { 'RequestItems' => {'User' => { 'Keys' => keys }}}
295
+
296
+ 25.times do |i|
297
+ subject.put_item({'TableName' => 'User',
298
+ 'Item' => { 'id' => { 'S' => i.to_s },
299
+ 'payload' => { 'S' => ('x' * 50 * 1024) }}})
300
+ keys << { 'id' => { 'S' => i.to_s } }
301
+ end
302
+ response = subject.process('BatchGetItem', request);
303
+ response['UnprocessedKeys']['User']['Keys'].should_not be_empty
304
+ end
305
+
292
306
  it 'should return items' do
293
307
  response = subject.process('BatchGetItem', { 'RequestItems' =>
294
308
  {
@@ -102,7 +102,7 @@ module FakeDynamo
102
102
  subject.ne_filter([{'S' => 'bcd'}], s_attr, false).should be_false
103
103
  subject.ne_filter([{'S' => '10'}], n_attr, false).should be_false
104
104
  subject.ne_filter([{'S' => 'xx'}], s_attr, false).should be_true
105
- subject.ne_filter([{'S' => '10'}], n_attr, false).should be_false
105
+ subject.ne_filter([{'N' => '10.0'}], n_attr, false).should be_false
106
106
  subject.ne_filter([{'B' => bstr}], b_attr, false).should be_false
107
107
  end
108
108
 
@@ -48,7 +48,7 @@ module FakeDynamo
48
48
  it "should delete values" do
49
49
  subject.attributes['friends'] = Attribute.new('friends', ["1", "2"], "NS")
50
50
  subject.delete('friends', { "NS" => ["2", "4"]})
51
- subject.attributes['friends'].value.should == ["1"]
51
+ subject.attributes['friends'].value.should == [Num.new("1")]
52
52
  end
53
53
  end
54
54
 
@@ -73,7 +73,13 @@ module FakeDynamo
73
73
  it "should increment numbers" do
74
74
  subject.attributes['number'] = Attribute.new('number', '5', 'N')
75
75
  subject.add('number', { 'N' => '3'})
76
- subject.attributes['number'].value.should eq('8')
76
+ subject.attributes['number'].value.should eq(Num.new('8'))
77
+ end
78
+
79
+ it "should decrement numbers" do
80
+ subject.attributes['number'] = Attribute.new('number', '5', 'N')
81
+ subject.add('number', { 'N' => '-3'})
82
+ subject.attributes['number'].value.should eq(Num.new('2'))
77
83
  end
78
84
 
79
85
  it "should handle sets" do
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+ describe Num do
5
+ it 'should validate number' do
6
+ ['10000000000000000000000000000000000101',
7
+ '0.1',
8
+ '1000000000000000000000.0000000000000101',
9
+ '.00000000000000000000000000000000000001011',
10
+ '.10000000000000000000000000000000001011',
11
+ '.1e126',
12
+ '-.1e126',
13
+ '-3'].each do |n|
14
+ Num.new(n)
15
+ end
16
+ end
17
+
18
+ it 'should raise on number larger that 38 significant digits' do
19
+ ['1000000000000000000000.00000000000001011',
20
+ '1.00000000000000000000000000000000001011',
21
+ '.100000000000000000000000000000000001011'].each do |n|
22
+ expect { Num.new(n) }.to raise_error(ValidationException, /significant/)
23
+ end
24
+ end
25
+
26
+ it 'shoud raise on overflow' do
27
+ ['.1e127',
28
+ '-.1e127'].each do |n|
29
+ expect { Num.new(n) }.to raise_error(ValidationException, /Number .*flow/)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -115,15 +115,16 @@ module FakeDynamo
115
115
  'AttributeName2' => { 'N' => "3" },
116
116
  'AttributeName3' => { 'N' => "4.44444" }
117
117
  }})
118
- response = subject.get_item({'TableName' => 'Table1',
119
- 'Key' => {
120
- 'AttributeName1' => { 'S' => 'test' },
121
- 'AttributeName2' => { 'N' => '3' }
122
- }})
123
-
124
- response['Item']['AttributeName3'].should eq('N' => '4.44444')
125
118
 
119
+ ['3', '3.0', '3e0', '.3e1', '003'].each do |n|
120
+ response = subject.get_item({'TableName' => 'Table1',
121
+ 'Key' => {
122
+ 'AttributeName1' => { 'S' => 'test' },
123
+ 'AttributeName2' => { 'N' => n }
124
+ }})
126
125
 
126
+ response['Item']['AttributeName3'].should eq('N' => '4.44444')
127
+ end
127
128
  end
128
129
 
129
130
  it 'should fail if range key is not present' do
@@ -491,6 +492,21 @@ module FakeDynamo
491
492
  response = t.query(query)
492
493
  response['Items'].first.keys.size.should eq(4)
493
494
  end
495
+
496
+ it 'should return last evaluated key when the item processed size exceeds 1 mb' do
497
+ t = Table.new(data)
498
+ t.put_item(item)
499
+ 25.times do |i|
500
+ t.put_item({'TableName' => 'User',
501
+ 'Item' => { 'AttributeName1' => { 'S' => '1' },
502
+ 'AttributeName2' => { 'N' => i},
503
+ 'payload' => { 'S' => ('x' * 50 * 1024) }}})
504
+ end
505
+ query['KeyConditions']['AttributeName1']['AttributeValueList'] = [{'S' => '1'}]
506
+ query['KeyConditions'].delete('AttributeName2')
507
+ response = t.query(query)
508
+ response['LastEvaluatedKey'].should_not be_empty
509
+ end
494
510
  end
495
511
 
496
512
 
data/spec/spec_helper.rb CHANGED
@@ -24,3 +24,4 @@ module FakeDynamo
24
24
  end
25
25
 
26
26
  FakeDynamo::Storage.db_path = '/tmp/test_db.fdb'
27
+ FakeDynamo::Logger.setup(:debug)
metadata CHANGED
@@ -1,49 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fake_dynamo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
5
- prerelease:
4
+ version: 0.2.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Anantha Kumaran
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-05-02 00:00:00.000000000 Z
11
+ date: 2013-06-04 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: sinatra
16
- requirement: &70292157821000 !ruby/object:Gem::Requirement
17
- none: false
15
+ requirement: !ruby/object:Gem::Requirement
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *70292157821000
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
25
27
  - !ruby/object:Gem::Dependency
26
28
  name: activesupport
27
- requirement: &70292157820120 !ruby/object:Gem::Requirement
28
- none: false
29
+ requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
- - - ! '>='
31
+ - - '>='
31
32
  - !ruby/object:Gem::Version
32
33
  version: '0'
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *70292157820120
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
36
41
  - !ruby/object:Gem::Dependency
37
42
  name: json
38
- requirement: &70292157812900 !ruby/object:Gem::Requirement
39
- none: false
43
+ requirement: !ruby/object:Gem::Requirement
40
44
  requirements:
41
- - - ! '>='
45
+ - - '>='
42
46
  - !ruby/object:Gem::Version
43
47
  version: '0'
44
48
  type: :runtime
45
49
  prerelease: false
46
- version_requirements: *70292157812900
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
47
55
  description:
48
56
  email:
49
57
  - ananthakumaran@gmail.com
@@ -73,7 +81,10 @@ files:
73
81
  - lib/fake_dynamo/key.rb
74
82
  - lib/fake_dynamo/key_schema.rb
75
83
  - lib/fake_dynamo/local_secondary_index.rb
84
+ - lib/fake_dynamo/logger.rb
85
+ - lib/fake_dynamo/num.rb
76
86
  - lib/fake_dynamo/projection.rb
87
+ - lib/fake_dynamo/sack.rb
77
88
  - lib/fake_dynamo/server.rb
78
89
  - lib/fake_dynamo/storage.rb
79
90
  - lib/fake_dynamo/table.rb
@@ -82,6 +93,7 @@ files:
82
93
  - spec/fake_dynamo/db_spec.rb
83
94
  - spec/fake_dynamo/filter_spec.rb
84
95
  - spec/fake_dynamo/item_spec.rb
96
+ - spec/fake_dynamo/num_spec.rb
85
97
  - spec/fake_dynamo/server_spec.rb
86
98
  - spec/fake_dynamo/storage_spec.rb
87
99
  - spec/fake_dynamo/table_spec.rb
@@ -89,35 +101,34 @@ files:
89
101
  - spec/spec_helper.rb
90
102
  homepage:
91
103
  licenses: []
104
+ metadata: {}
92
105
  post_install_message:
93
106
  rdoc_options: []
94
107
  require_paths:
95
108
  - lib
96
109
  required_ruby_version: !ruby/object:Gem::Requirement
97
- none: false
98
110
  requirements:
99
- - - ! '>='
111
+ - - '>='
100
112
  - !ruby/object:Gem::Version
101
113
  version: 1.9.0
102
114
  required_rubygems_version: !ruby/object:Gem::Requirement
103
- none: false
104
115
  requirements:
105
- - - ! '>='
116
+ - - '>='
106
117
  - !ruby/object:Gem::Version
107
118
  version: '0'
108
119
  requirements: []
109
120
  rubyforge_project:
110
- rubygems_version: 1.8.15
121
+ rubygems_version: 2.0.3
111
122
  signing_key:
112
- specification_version: 3
123
+ specification_version: 4
113
124
  summary: local hosted, inmemory fake dynamodb
114
125
  test_files:
115
126
  - spec/fake_dynamo/db_spec.rb
116
127
  - spec/fake_dynamo/filter_spec.rb
117
128
  - spec/fake_dynamo/item_spec.rb
129
+ - spec/fake_dynamo/num_spec.rb
118
130
  - spec/fake_dynamo/server_spec.rb
119
131
  - spec/fake_dynamo/storage_spec.rb
120
132
  - spec/fake_dynamo/table_spec.rb
121
133
  - spec/fake_dynamo/validation_spec.rb
122
134
  - spec/spec_helper.rb
123
- has_rdoc: