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