fake_dynamo 0.1.4 → 0.2.0
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.
- data/Gemfile +1 -1
- data/Guardfile +3 -0
- data/lib/fake_dynamo/api.yml +1 -1
- data/lib/fake_dynamo/api_2012-08-10.yml +1555 -0
- data/lib/fake_dynamo/db.rb +38 -5
- data/lib/fake_dynamo/item.rb +10 -0
- data/lib/fake_dynamo/key.rb +2 -10
- data/lib/fake_dynamo/key_schema.rb +27 -8
- data/lib/fake_dynamo/local_secondary_index.rb +27 -0
- data/lib/fake_dynamo/projection.rb +30 -0
- data/lib/fake_dynamo/table.rb +174 -76
- data/lib/fake_dynamo/validation.rb +66 -14
- data/lib/fake_dynamo/version.rb +1 -1
- data/lib/fake_dynamo.rb +2 -0
- data/spec/fake_dynamo/db_spec.rb +147 -53
- data/spec/fake_dynamo/server_spec.rb +5 -2
- data/spec/fake_dynamo/storage_spec.rb +3 -1
- data/spec/fake_dynamo/table_spec.rb +161 -39
- data/spec/fake_dynamo/validation_spec.rb +17 -18
- metadata +25 -28
- checksums.yaml +0 -7
data/lib/fake_dynamo/db.rb
CHANGED
@@ -90,26 +90,32 @@ module FakeDynamo
|
|
90
90
|
|
91
91
|
def batch_get_item(data)
|
92
92
|
response = {}
|
93
|
+
consumed_capacity = {}
|
93
94
|
|
94
95
|
data['RequestItems'].each do |table_name, table_data|
|
95
96
|
table = find_table(table_name)
|
96
97
|
|
97
98
|
unless response[table_name]
|
98
|
-
response[table_name] =
|
99
|
+
response[table_name] = []
|
100
|
+
set_consumed_capacity(consumed_capacity, table, data)
|
99
101
|
end
|
100
102
|
|
101
103
|
table_data['Keys'].each do |key|
|
102
104
|
if item_hash = table.get_raw_item(key, table_data['AttributesToGet'])
|
103
|
-
response[table_name]
|
105
|
+
response[table_name] << item_hash
|
104
106
|
end
|
105
107
|
end
|
106
108
|
end
|
107
109
|
|
108
|
-
{ 'Responses' => response, 'UnprocessedKeys' => {}}
|
110
|
+
response = { 'Responses' => response, 'UnprocessedKeys' => {} }
|
111
|
+
merge_consumed_capacity(consumed_capacity, response)
|
109
112
|
end
|
110
113
|
|
111
114
|
def batch_write_item(data)
|
112
115
|
response = {}
|
116
|
+
consumed_capacity = {}
|
117
|
+
item_collection_metrics = {}
|
118
|
+
merge_metrics = false
|
113
119
|
items = {}
|
114
120
|
request_count = 0
|
115
121
|
|
@@ -118,6 +124,7 @@ module FakeDynamo
|
|
118
124
|
table = find_table(table_name)
|
119
125
|
|
120
126
|
items[table.name] ||= {}
|
127
|
+
item_collection_metrics[table.name] ||= []
|
121
128
|
|
122
129
|
requests.each do |request|
|
123
130
|
if request['PutRequest']
|
@@ -139,21 +146,47 @@ module FakeDynamo
|
|
139
146
|
# real modification
|
140
147
|
items.each do |table_name, requests|
|
141
148
|
table = find_table(table_name)
|
149
|
+
item_collection_metrics[table.name] ||= []
|
150
|
+
|
142
151
|
requests.each do |key, value|
|
143
152
|
if value == :delete
|
144
153
|
table.batch_delete(key)
|
145
154
|
else
|
146
155
|
table.batch_put(value)
|
147
156
|
end
|
157
|
+
|
158
|
+
unless (metrics = Item.from_key(key).collection_metrics(data)).empty?
|
159
|
+
merge_metrics = true
|
160
|
+
item_collection_metrics[table.name] << metrics['ItemCollectionMetrics']
|
161
|
+
end
|
162
|
+
|
148
163
|
end
|
149
|
-
|
164
|
+
set_consumed_capacity(consumed_capacity, table, data)
|
150
165
|
end
|
151
166
|
|
152
|
-
|
167
|
+
response = { 'UnprocessedItems' => {} }
|
168
|
+
response = merge_consumed_capacity(consumed_capacity, response)
|
169
|
+
if merge_metrics
|
170
|
+
response.merge!({'ItemCollectionMetrics' => item_collection_metrics})
|
171
|
+
end
|
172
|
+
response
|
153
173
|
end
|
154
174
|
|
155
175
|
private
|
156
176
|
|
177
|
+
def set_consumed_capacity(consumed_capacity, table, data)
|
178
|
+
unless (capacity = table.consumed_capacity(data)).empty?
|
179
|
+
consumed_capacity[table.name] = capacity['ConsumedCapacity']
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def merge_consumed_capacity(consumed_capacity, response)
|
184
|
+
unless consumed_capacity.empty?
|
185
|
+
response['ConsumedCapacity'] = consumed_capacity.values
|
186
|
+
end
|
187
|
+
response
|
188
|
+
end
|
189
|
+
|
157
190
|
def check_item_conflict(items, table_name, key)
|
158
191
|
if items[table_name][key]
|
159
192
|
raise ValidationException, 'Provided list of item keys contains duplicates'
|
data/lib/fake_dynamo/item.rb
CHANGED
@@ -104,5 +104,15 @@ module FakeDynamo
|
|
104
104
|
attributes[name] = attribute
|
105
105
|
end
|
106
106
|
end
|
107
|
+
|
108
|
+
def collection_metrics(data)
|
109
|
+
if data['ReturnItemCollectionMetrics'] == 'SIZE'
|
110
|
+
{ 'ItemCollectionMetrics' =>
|
111
|
+
{ 'ItemCollectionKey' => key.primary.as_hash,
|
112
|
+
'SizeEstimateRangeGB' => [ 0, 1 ] } }
|
113
|
+
else
|
114
|
+
{}
|
115
|
+
end
|
116
|
+
end
|
107
117
|
end
|
108
118
|
end
|
data/lib/fake_dynamo/key.rb
CHANGED
@@ -9,10 +9,10 @@ module FakeDynamo
|
|
9
9
|
def from_data(key_data, key_schema)
|
10
10
|
key = Key.new
|
11
11
|
validate_key_data(key_data, key_schema)
|
12
|
-
key.primary = Attribute.from_hash(key_schema.hash_key.name, key_data[
|
12
|
+
key.primary = Attribute.from_hash(key_schema.hash_key.name, key_data[key_schema.hash_key.name])
|
13
13
|
|
14
14
|
if key_schema.range_key
|
15
|
-
key.range = Attribute.from_hash(key_schema.range_key.name, key_data[
|
15
|
+
key.range = Attribute.from_hash(key_schema.range_key.name, key_data[key_schema.range_key.name])
|
16
16
|
end
|
17
17
|
key
|
18
18
|
end
|
@@ -60,14 +60,6 @@ module FakeDynamo
|
|
60
60
|
result
|
61
61
|
end
|
62
62
|
|
63
|
-
def as_key_hash
|
64
|
-
result = { 'HashKeyElement' => { @primary.type => @primary.value }}
|
65
|
-
if @range
|
66
|
-
result.merge!({'RangeKeyElement' => { @range.type => @range.value }})
|
67
|
-
end
|
68
|
-
result
|
69
|
-
end
|
70
|
-
|
71
63
|
def <=>(other)
|
72
64
|
[primary, range] <=> [other.primary, other.range]
|
73
65
|
end
|
@@ -3,23 +3,42 @@ module FakeDynamo
|
|
3
3
|
|
4
4
|
attr_accessor :hash_key, :range_key
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
extract_values(
|
6
|
+
def initialize(key_schema, attribute_definitions)
|
7
|
+
extract_values(key_schema, attribute_definitions)
|
8
8
|
end
|
9
9
|
|
10
10
|
def description
|
11
|
-
description = {
|
11
|
+
description = [{'AttributeName' => hash_key.name, 'KeyType' => 'HASH'}]
|
12
12
|
if range_key
|
13
|
-
description['
|
13
|
+
description << [{'AttributeName' => range_key.name, 'KeyType' => 'RANGE'}]
|
14
14
|
end
|
15
15
|
description
|
16
16
|
end
|
17
17
|
|
18
|
+
def keys
|
19
|
+
result = [hash_key.name]
|
20
|
+
if range_key
|
21
|
+
result << range_key.name
|
22
|
+
end
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
18
26
|
private
|
19
|
-
def extract_values(
|
20
|
-
|
21
|
-
|
22
|
-
|
27
|
+
def extract_values(key_schema, attribute_definitions)
|
28
|
+
hash_key_name = find(key_schema, 'KeyType', 'HASH', 'AttributeName')
|
29
|
+
hash_key_type = find(attribute_definitions, 'AttributeName', hash_key_name, 'AttributeType')
|
30
|
+
@hash_key = Attribute.new(hash_key_name, nil, hash_key_type)
|
31
|
+
if range_key_name = find(key_schema, 'KeyType', 'RANGE', 'AttributeName', false)
|
32
|
+
range_key_type = find(attribute_definitions, 'AttributeName', range_key_name, 'AttributeType')
|
33
|
+
@range_key = Attribute.new(range_key_name, nil, range_key_type)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find(list, key, value, pluck, raise_on_error = true)
|
38
|
+
if element = list.find { |e| e[key] == value }
|
39
|
+
element[pluck]
|
40
|
+
elsif raise_on_error
|
41
|
+
raise ValidationException, 'Some index key attributes are not defined in AttributeDefinitions'
|
23
42
|
end
|
24
43
|
end
|
25
44
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class LocalSecondaryIndex
|
3
|
+
extend Validation
|
4
|
+
|
5
|
+
attr_accessor :name, :key_schema, :projection
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def from_data(index_data, attribute_definitions, table_key_schema)
|
9
|
+
index = LocalSecondaryIndex.new
|
10
|
+
index.name = index_data['IndexName']
|
11
|
+
index.key_schema = KeySchema.new(index_data['KeySchema'], attribute_definitions)
|
12
|
+
index.projection = Projection.from_data(index_data['Projection'])
|
13
|
+
validate_range_key(index.key_schema)
|
14
|
+
validate_hash_key(index.key_schema, table_key_schema)
|
15
|
+
index
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
{'IndexName' => name,
|
21
|
+
'IndexSizeBytes' => 0,
|
22
|
+
'ItemCount' => 0,
|
23
|
+
'KeySchema' => key_schema.description,
|
24
|
+
'Projection' => projection.description}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class Projection
|
3
|
+
extend Validation
|
4
|
+
attr_accessor :type, :non_key_attributes
|
5
|
+
|
6
|
+
def initialize(type, non_key_attributes)
|
7
|
+
@type, @non_key_attributes = type, non_key_attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def from_data(data)
|
12
|
+
projection = Projection.new(data['ProjectionType'], data['NonKeyAttributes'])
|
13
|
+
validate_projection(projection)
|
14
|
+
projection
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
{'ProjectionType' => type}.merge(non_key_attributes_description)
|
20
|
+
end
|
21
|
+
|
22
|
+
def non_key_attributes_description
|
23
|
+
if non_key_attributes
|
24
|
+
{'NonKeyAttributes' => @non_key_attributes}
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/fake_dynamo/table.rb
CHANGED
@@ -4,8 +4,8 @@ module FakeDynamo
|
|
4
4
|
include Filter
|
5
5
|
|
6
6
|
attr_accessor :creation_date_time, :read_capacity_units, :write_capacity_units,
|
7
|
-
:name, :status, :key_schema, :items, :size_bytes,
|
8
|
-
:last_decreased_time
|
7
|
+
:name, :status, :attribute_definitions, :key_schema, :items, :size_bytes,
|
8
|
+
:local_secondary_indexes, :last_increased_time, :last_decreased_time
|
9
9
|
|
10
10
|
def initialize(data)
|
11
11
|
extract_values(data)
|
@@ -15,21 +15,48 @@ module FakeDynamo
|
|
15
15
|
def description
|
16
16
|
{
|
17
17
|
'TableDescription' => {
|
18
|
+
'AttributeDefinitions' => attribute_definitions.map(&:description),
|
18
19
|
'CreationDateTime' => creation_date_time,
|
19
20
|
'KeySchema' => key_schema.description,
|
20
|
-
'ProvisionedThroughput' =>
|
21
|
-
'ReadCapacityUnits' => read_capacity_units,
|
22
|
-
'WriteCapacityUnits' => write_capacity_units
|
23
|
-
},
|
21
|
+
'ProvisionedThroughput' => throughput_description,
|
24
22
|
'TableName' => name,
|
25
|
-
'TableStatus' => status
|
26
|
-
|
23
|
+
'TableStatus' => status,
|
24
|
+
'ItemCount' => items.count,
|
25
|
+
'TableSizeBytes' => size_bytes
|
26
|
+
}.merge(local_secondary_indexes_description)
|
27
27
|
}
|
28
28
|
end
|
29
29
|
|
30
|
+
def throughput_description
|
31
|
+
result = {
|
32
|
+
'NumberOfDecreasesToday' => 0,
|
33
|
+
'ReadCapacityUnits' => read_capacity_units,
|
34
|
+
'WriteCapacityUnits' => write_capacity_units
|
35
|
+
}
|
36
|
+
|
37
|
+
if last_increased_time
|
38
|
+
result['LastIncreaseDateTime'] = @last_increased_time
|
39
|
+
end
|
40
|
+
|
41
|
+
if last_decreased_time
|
42
|
+
result['LastDecreaseDateTime'] = @last_decreased_time
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def local_secondary_indexes_description
|
49
|
+
if local_secondary_indexes
|
50
|
+
{ 'LocalSecondaryIndexes' => local_secondary_indexes.map(&:description) }
|
51
|
+
else
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
30
56
|
def create_table_data
|
31
57
|
{
|
32
58
|
'TableName' => name,
|
59
|
+
'AttributeDefinitions' => attribute_definitions.map(&:description),
|
33
60
|
'KeySchema' => key_schema.description,
|
34
61
|
'ProvisionedThroughput' => {
|
35
62
|
'ReadCapacityUnits' => read_capacity_units,
|
@@ -45,13 +72,8 @@ module FakeDynamo
|
|
45
72
|
}
|
46
73
|
end
|
47
74
|
|
48
|
-
def size_description
|
49
|
-
{ 'ItemCount' => items.count,
|
50
|
-
'TableSizeBytes' => size_bytes }
|
51
|
-
end
|
52
|
-
|
53
75
|
def describe_table
|
54
|
-
{ 'Table' => description['TableDescription'] }
|
76
|
+
{ 'Table' => description['TableDescription'] }
|
55
77
|
end
|
56
78
|
|
57
79
|
def activate
|
@@ -78,16 +100,7 @@ module FakeDynamo
|
|
78
100
|
|
79
101
|
@read_capacity_units, @write_capacity_units = read_capacity_units, write_capacity_units
|
80
102
|
|
81
|
-
response = description
|
82
|
-
|
83
|
-
if last_increased_time
|
84
|
-
response['TableDescription']['ProvisionedThroughput']['LastIncreaseDateTime'] = @last_increased_time
|
85
|
-
end
|
86
|
-
|
87
|
-
if last_decreased_time
|
88
|
-
response['TableDescription']['ProvisionedThroughput']['LastDecreaseDateTime'] = @last_decreased_time
|
89
|
-
end
|
90
|
-
|
103
|
+
response = description
|
91
104
|
response['TableDescription']['TableStatus'] = 'UPDATING'
|
92
105
|
response
|
93
106
|
end
|
@@ -98,7 +111,7 @@ module FakeDynamo
|
|
98
111
|
check_conditions(old_item, data['Expected'])
|
99
112
|
@items[item.key] = item
|
100
113
|
|
101
|
-
|
114
|
+
return_values(data, old_item).merge(item.collection_metrics(data))
|
102
115
|
end
|
103
116
|
|
104
117
|
def batch_put_request(data)
|
@@ -110,7 +123,7 @@ module FakeDynamo
|
|
110
123
|
end
|
111
124
|
|
112
125
|
def get_item(data)
|
113
|
-
response = consumed_capacity
|
126
|
+
response = consumed_capacity(data)
|
114
127
|
if item_hash = get_raw_item(data['Key'], data['AttributesToGet'])
|
115
128
|
response.merge!('Item' => item_hash)
|
116
129
|
end
|
@@ -142,7 +155,12 @@ module FakeDynamo
|
|
142
155
|
check_conditions(item, data['Expected'])
|
143
156
|
|
144
157
|
@items.delete(key) if item
|
145
|
-
|
158
|
+
if !item
|
159
|
+
item = Item.from_key(key)
|
160
|
+
consumed_capacity(data).merge(item.collection_metrics(data))
|
161
|
+
else
|
162
|
+
return_values(data, item).merge(consumed_capacity(data)).merge(item.collection_metrics(data))
|
163
|
+
end
|
146
164
|
end
|
147
165
|
|
148
166
|
def batch_delete_request(data)
|
@@ -159,10 +177,11 @@ module FakeDynamo
|
|
159
177
|
check_conditions(item, data['Expected'])
|
160
178
|
|
161
179
|
unless item
|
180
|
+
item = Item.from_key(key)
|
162
181
|
if create_item?(data)
|
163
|
-
|
182
|
+
@items[key] = item
|
164
183
|
else
|
165
|
-
return consumed_capacity
|
184
|
+
return consumed_capacity(data).merge(item.collection_metrics(data))
|
166
185
|
end
|
167
186
|
item_created = true
|
168
187
|
end
|
@@ -170,8 +189,10 @@ module FakeDynamo
|
|
170
189
|
old_item = deep_copy(item)
|
171
190
|
begin
|
172
191
|
old_hash = item.as_hash
|
173
|
-
data['AttributeUpdates']
|
174
|
-
|
192
|
+
if attribute_updates = data['AttributeUpdates']
|
193
|
+
attribute_updates.each do |name, update_data|
|
194
|
+
item.update(name, update_data)
|
195
|
+
end
|
175
196
|
end
|
176
197
|
rescue => e
|
177
198
|
if item_created
|
@@ -182,7 +203,7 @@ module FakeDynamo
|
|
182
203
|
raise e
|
183
204
|
end
|
184
205
|
|
185
|
-
|
206
|
+
return_values(data, old_hash, item).merge(item.collection_metrics(data))
|
186
207
|
end
|
187
208
|
|
188
209
|
def deep_copy(x)
|
@@ -190,66 +211,100 @@ module FakeDynamo
|
|
190
211
|
end
|
191
212
|
|
192
213
|
def query(data)
|
193
|
-
|
194
|
-
|
214
|
+
range_key_present
|
215
|
+
select_and_attributes_to_get_present(data)
|
216
|
+
validate_limit(data)
|
217
|
+
|
218
|
+
index = nil
|
219
|
+
if index_name = data['IndexName']
|
220
|
+
index = local_secondary_indexes.find { |i| i.name == index_name }
|
221
|
+
raise ValidationException, "The provided starting key is invalid" unless index
|
222
|
+
schema = index.key_schema
|
223
|
+
else
|
224
|
+
schema = key_schema
|
195
225
|
end
|
196
226
|
|
197
|
-
|
198
|
-
|
227
|
+
hash_condition = data['KeyConditions'][schema.hash_key.name]
|
228
|
+
validate_hash_condition(hash_condition)
|
199
229
|
|
200
|
-
hash_attribute = Attribute.from_hash(
|
230
|
+
hash_attribute = Attribute.from_hash(schema.hash_key.name, hash_condition['AttributeValueList'].first)
|
201
231
|
matched_items = get_items_by_hash_key(hash_attribute)
|
202
232
|
|
203
233
|
forward = data.has_key?('ScanIndexForward') ? data['ScanIndexForward'] : true
|
204
|
-
matched_items = drop_till_start(matched_items, data['ExclusiveStartKey'], forward)
|
234
|
+
matched_items = drop_till_start(matched_items, data['ExclusiveStartKey'], forward, schema)
|
205
235
|
|
206
|
-
if data['
|
207
|
-
|
236
|
+
if !(range_condition = data['KeyConditions'].clone.tap { |h| h.delete(schema.hash_key.name) }).empty?
|
237
|
+
validate_range_condition(range_condition, schema)
|
238
|
+
conditions = range_condition
|
208
239
|
else
|
209
240
|
conditions = {}
|
210
241
|
end
|
211
242
|
|
212
|
-
|
213
|
-
|
214
|
-
response = {
|
215
|
-
'Count' => result.size,
|
216
|
-
'ConsumedCapacityUnits' => 1 }
|
243
|
+
results, last_evaluated_item, _ = filter(matched_items, conditions, data['Limit'], true)
|
217
244
|
|
218
|
-
|
219
|
-
|
220
|
-
end
|
245
|
+
response = {'Count' => results.size}.merge(consumed_capacity(data))
|
246
|
+
merge_items(response, data, results, index)
|
221
247
|
|
222
248
|
if last_evaluated_item
|
223
|
-
response['LastEvaluatedKey'] = last_evaluated_item.key.
|
249
|
+
response['LastEvaluatedKey'] = last_evaluated_item.key.as_hash
|
224
250
|
end
|
225
251
|
response
|
226
252
|
end
|
227
253
|
|
228
254
|
def scan(data)
|
229
|
-
|
255
|
+
select_and_attributes_to_get_present(data)
|
230
256
|
validate_limit(data)
|
231
257
|
conditions = data['ScanFilter'] || {}
|
232
|
-
all_items = drop_till_start(items.values, data['ExclusiveStartKey'], true)
|
233
|
-
|
258
|
+
all_items = drop_till_start(items.values, data['ExclusiveStartKey'], true, key_schema)
|
259
|
+
results, last_evaluated_item, scaned_count = filter(all_items, conditions, data['Limit'], false)
|
234
260
|
response = {
|
235
|
-
'Count' =>
|
236
|
-
'ScannedCount' => scaned_count
|
237
|
-
'ConsumedCapacityUnits' => 1 }
|
261
|
+
'Count' => results.size,
|
262
|
+
'ScannedCount' => scaned_count}.merge(consumed_capacity(data))
|
238
263
|
|
239
|
-
|
240
|
-
response['Items'] = result.map { |r| filter_attributes(r, data['AttributesToGet']) }
|
241
|
-
end
|
264
|
+
merge_items(response, data, results)
|
242
265
|
|
243
266
|
if last_evaluated_item
|
244
|
-
response['LastEvaluatedKey'] = last_evaluated_item.key.
|
267
|
+
response['LastEvaluatedKey'] = last_evaluated_item.key.as_hash
|
268
|
+
end
|
269
|
+
|
270
|
+
response
|
271
|
+
end
|
272
|
+
|
273
|
+
def merge_items(response, data, results, index = nil)
|
274
|
+
if data['Select'] != 'COUNT'
|
275
|
+
attributes_to_get = nil # select everything
|
276
|
+
|
277
|
+
if data['AttributesToGet']
|
278
|
+
attributes_to_get = data['AttributesToGet']
|
279
|
+
elsif data['Select'] == 'ALL_PROJECTED_ATTRIBUTES'
|
280
|
+
attributes_to_get = projected_attributes(index)
|
281
|
+
end
|
282
|
+
|
283
|
+
response['Items'] = results.map { |r| filter_attributes(r, attributes_to_get) }
|
245
284
|
end
|
246
285
|
|
247
286
|
response
|
248
287
|
end
|
249
288
|
|
250
|
-
def
|
251
|
-
if
|
252
|
-
raise ValidationException, "
|
289
|
+
def projected_attributes(index)
|
290
|
+
if !index
|
291
|
+
raise ValidationException, "ALL_PROJECTED_ATTRIBUTES can be used only when Querying using an IndexName"
|
292
|
+
else
|
293
|
+
case index.projection.type
|
294
|
+
when 'ALL'
|
295
|
+
nil
|
296
|
+
when 'KEYS_ONLY'
|
297
|
+
(key_schema.keys + index.key_schema.keys).uniq
|
298
|
+
when 'INCLUDE'
|
299
|
+
(key_schema.keys + index.key_schema.keys + index.projection.non_key_attributes).uniq
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def select_and_attributes_to_get_present(data)
|
305
|
+
select = data['Select']
|
306
|
+
if select and data['AttributesToGet'] and (select != 'SPECIFIC_ATTRIBUTES')
|
307
|
+
raise ValidationException, "Cannot specify the AttributesToGet when choosing to get only the #{select}"
|
253
308
|
end
|
254
309
|
end
|
255
310
|
|
@@ -259,7 +314,7 @@ module FakeDynamo
|
|
259
314
|
end
|
260
315
|
end
|
261
316
|
|
262
|
-
def drop_till_start(all_items, start_key_hash, forward)
|
317
|
+
def drop_till_start(all_items, start_key_hash, forward, schema)
|
263
318
|
all_items = all_items.sort_by { |item| item.key }
|
264
319
|
|
265
320
|
unless forward
|
@@ -267,7 +322,7 @@ module FakeDynamo
|
|
267
322
|
end
|
268
323
|
|
269
324
|
if start_key_hash
|
270
|
-
start_key = Key.from_data(start_key_hash,
|
325
|
+
start_key = Key.from_data(start_key_hash, schema)
|
271
326
|
all_items.drop_while do |item|
|
272
327
|
if forward
|
273
328
|
item.key <= start_key
|
@@ -296,6 +351,8 @@ module FakeDynamo
|
|
296
351
|
end
|
297
352
|
end
|
298
353
|
|
354
|
+
scaned_count += 1
|
355
|
+
|
299
356
|
if select
|
300
357
|
result << item
|
301
358
|
if (limit -= 1) == 0
|
@@ -303,8 +360,6 @@ module FakeDynamo
|
|
303
360
|
break
|
304
361
|
end
|
305
362
|
end
|
306
|
-
|
307
|
-
scaned_count += 1
|
308
363
|
end
|
309
364
|
[result, last_evaluated_item, scaned_count]
|
310
365
|
end
|
@@ -316,9 +371,13 @@ module FakeDynamo
|
|
316
371
|
end
|
317
372
|
|
318
373
|
def create_item?(data)
|
319
|
-
data['AttributeUpdates']
|
320
|
-
|
321
|
-
|
374
|
+
if attribute_updates = data['AttributeUpdates']
|
375
|
+
attribute_updates.any? do |name, update_data|
|
376
|
+
action = update_data['Action']
|
377
|
+
['PUT', 'ADD', nil].include? action
|
378
|
+
end
|
379
|
+
else
|
380
|
+
true
|
322
381
|
end
|
323
382
|
end
|
324
383
|
|
@@ -352,17 +411,23 @@ module FakeDynamo
|
|
352
411
|
raise 'unknown return value'
|
353
412
|
end
|
354
413
|
|
355
|
-
unless result.empty?
|
356
|
-
|
414
|
+
result = unless result.empty?
|
415
|
+
{ 'Attributes' => result }
|
416
|
+
else
|
417
|
+
{}
|
418
|
+
end
|
419
|
+
|
420
|
+
result.merge(consumed_capacity(data))
|
421
|
+
end
|
422
|
+
|
423
|
+
def consumed_capacity(data)
|
424
|
+
if data['ReturnConsumedCapacity'] == 'TOTAL'
|
425
|
+
{'ConsumedCapacity' => { 'CapacityUnits' => 1, 'TableName' => @name }}
|
357
426
|
else
|
358
427
|
{}
|
359
428
|
end
|
360
429
|
end
|
361
430
|
|
362
|
-
def consumed_capacity
|
363
|
-
{ 'ConsumedCapacityUnits' => 1 }
|
364
|
-
end
|
365
|
-
|
366
431
|
def check_conditions(old_item, conditions)
|
367
432
|
return unless conditions
|
368
433
|
|
@@ -403,8 +468,12 @@ module FakeDynamo
|
|
403
468
|
|
404
469
|
def extract_values(data)
|
405
470
|
@name = data['TableName']
|
406
|
-
@key_schema = KeySchema.new(data['KeySchema'])
|
471
|
+
@key_schema = KeySchema.new(data['KeySchema'], data['AttributeDefinitions'])
|
472
|
+
set_local_secondary_indexes(data)
|
473
|
+
@attribute_definitions = data['AttributeDefinitions'].map(&Attribute.method(:from_data))
|
407
474
|
set_throughput(data['ProvisionedThroughput'])
|
475
|
+
|
476
|
+
validate_attribute_definitions
|
408
477
|
end
|
409
478
|
|
410
479
|
def set_throughput(throughput)
|
@@ -412,5 +481,34 @@ module FakeDynamo
|
|
412
481
|
@write_capacity_units = throughput['WriteCapacityUnits']
|
413
482
|
end
|
414
483
|
|
484
|
+
def set_local_secondary_indexes(data)
|
485
|
+
if indexes_data = data['LocalSecondaryIndexes']
|
486
|
+
@local_secondary_indexes = indexes_data.map do |index|
|
487
|
+
LocalSecondaryIndex.from_data(index, data['AttributeDefinitions'], @key_schema)
|
488
|
+
end
|
489
|
+
validate_range_key(key_schema)
|
490
|
+
validate_index_names(@local_secondary_indexes)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
def validate_attribute_definitions
|
495
|
+
attribute_keys = @attribute_definitions.map(&:name)
|
496
|
+
used_keys = @key_schema.keys
|
497
|
+
if @local_secondary_indexes
|
498
|
+
used_keys += @local_secondary_indexes.map(&:key_schema).map(&:keys).flatten
|
499
|
+
end
|
500
|
+
|
501
|
+
used_keys.uniq!
|
502
|
+
|
503
|
+
if used_keys.uniq.size != attribute_keys.size
|
504
|
+
raise ValidationException, "Some AttributeDefinitions are not used AttributeDefinitions: #{attribute_keys.inspect}, keys used: #{used_keys.inspect}"
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
def range_key_present
|
509
|
+
unless key_schema.range_key
|
510
|
+
raise ValidationException, "Query can be performed only on a table with a HASH,RANGE key schema"
|
511
|
+
end
|
512
|
+
end
|
415
513
|
end
|
416
514
|
end
|