fake_dynamo 0.2.1 → 0.2.2

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.
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: