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 +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +1 -0
- data/README.md +13 -5
- data/bin/fake_dynamo +10 -2
- data/lib/fake_dynamo.rb +5 -2
- data/lib/fake_dynamo/api_2012-08-10.yml +6 -0
- data/lib/fake_dynamo/attribute.rb +37 -42
- data/lib/fake_dynamo/db.rb +16 -4
- data/lib/fake_dynamo/filter.rb +1 -5
- data/lib/fake_dynamo/item.rb +5 -6
- data/lib/fake_dynamo/logger.rb +24 -0
- data/lib/fake_dynamo/num.rb +63 -0
- data/lib/fake_dynamo/sack.rb +18 -0
- data/lib/fake_dynamo/server.rb +9 -5
- data/lib/fake_dynamo/storage.rb +8 -4
- data/lib/fake_dynamo/table.rb +71 -10
- data/lib/fake_dynamo/validation.rb +1 -1
- data/lib/fake_dynamo/version.rb +1 -1
- data/spec/fake_dynamo/db_spec.rb +14 -0
- data/spec/fake_dynamo/filter_spec.rb +1 -1
- data/spec/fake_dynamo/item_spec.rb +8 -2
- data/spec/fake_dynamo/num_spec.rb +33 -0
- data/spec/fake_dynamo/table_spec.rb +23 -7
- data/spec/spec_helper.rb +1 -0
- metadata +33 -22
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
data/.travis.yml
CHANGED
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.
|
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,
|
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 '
|
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, @
|
6
|
+
@name, @type = name, type
|
7
|
+
validate_name!
|
8
|
+
return unless value
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
10
|
+
@value = decode(value)
|
11
|
+
validate_value!
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
-
|
67
|
+
@value == attribute.value
|
69
68
|
end
|
70
69
|
|
71
70
|
def <=>(other)
|
72
|
-
|
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 ^
|
81
|
+
name.hash ^ value.hash ^ type.hash
|
87
82
|
end
|
88
83
|
|
89
84
|
class << self
|
data/lib/fake_dynamo/db.rb
CHANGED
@@ -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
|
105
|
-
|
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
|
210
|
+
tables[table_name] or raise ResourceNotFoundException, "Table: #{table_name} not found"
|
199
211
|
end
|
200
212
|
|
201
213
|
def check_max_request(count)
|
data/lib/fake_dynamo/filter.rb
CHANGED
@@ -23,11 +23,7 @@ module FakeDynamo
|
|
23
23
|
return false if target_attribute.type != value_attribute.type
|
24
24
|
end
|
25
25
|
|
26
|
-
|
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)
|
data/lib/fake_dynamo/item.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/fake_dynamo/server.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
18
|
-
pp
|
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
|
-
|
25
|
-
pp
|
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
|
data/lib/fake_dynamo/storage.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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|
|
data/lib/fake_dynamo/table.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
323
|
+
if data['Select'] == 'COUNT'
|
324
|
+
return projected_attributes(index)
|
292
325
|
end
|
293
326
|
|
294
|
-
|
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
|
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
|
data/lib/fake_dynamo/version.rb
CHANGED
data/spec/fake_dynamo/db_spec.rb
CHANGED
@@ -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([{'
|
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
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.
|
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-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
121
|
+
rubygems_version: 2.0.3
|
111
122
|
signing_key:
|
112
|
-
specification_version:
|
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:
|