Class: FakeDynamo::Table

Inherits:
Object
  • Object
show all
Includes:
Filter, Validation
Defined in:
lib/fake_dynamo/table.rb

Constant Summary

Constants included from Filter

Filter::INF

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Filter

#comparison_filter, #contains_filter, def_filter, #in_filter, #not_contains_filter, #not_null_filter, #null_filter, #validate_size, #validate_supported_types

Methods included from Validation

#add_errors, #api_config, #api_config_path, #api_input_spec, #available_operations, #param, #validate!, #validate_input, #validate_key_data, #validate_key_schema, #validate_operation, #validate_payload, #validate_request_size, #validate_spec, #validate_type

Constructor Details

#initialize(data) ⇒ Table

Returns a new instance of Table.



10
11
12
13
# File 'lib/fake_dynamo/table.rb', line 10

def initialize(data)
  extract_values(data)
  init
end

Instance Attribute Details

#creation_date_timeObject

Returns the value of attribute creation_date_time.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def creation_date_time
  @creation_date_time
end

#itemsObject

Returns the value of attribute items.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def items
  @items
end

#key_schemaObject

Returns the value of attribute key_schema.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def key_schema
  @key_schema
end

#last_decreased_timeObject

Returns the value of attribute last_decreased_time.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def last_decreased_time
  @last_decreased_time
end

#last_increased_timeObject

Returns the value of attribute last_increased_time.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def last_increased_time
  @last_increased_time
end

#nameObject

Returns the value of attribute name.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def name
  @name
end

#read_capacity_unitsObject

Returns the value of attribute read_capacity_units.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def read_capacity_units
  @read_capacity_units
end

#size_bytesObject

Returns the value of attribute size_bytes.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def size_bytes
  @size_bytes
end

#statusObject

Returns the value of attribute status.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def status
  @status
end

#write_capacity_unitsObject

Returns the value of attribute write_capacity_units.



6
7
8
# File 'lib/fake_dynamo/table.rb', line 6

def write_capacity_units
  @write_capacity_units
end

Instance Method Details

#activateObject



57
58
59
# File 'lib/fake_dynamo/table.rb', line 57

def activate
  @status = 'ACTIVE'
end

#batch_delete(key) ⇒ Object



152
153
154
# File 'lib/fake_dynamo/table.rb', line 152

def batch_delete(key)
  @items.delete(key)
end

#batch_delete_request(data) ⇒ Object



148
149
150
# File 'lib/fake_dynamo/table.rb', line 148

def batch_delete_request(data)
  Key.from_data(data['Key'], key_schema)
end

#batch_put(item) ⇒ Object



108
109
110
# File 'lib/fake_dynamo/table.rb', line 108

def batch_put(item)
  @items[item.key] = item
end

#batch_put_request(data) ⇒ Object



104
105
106
# File 'lib/fake_dynamo/table.rb', line 104

def batch_put_request(data)
  Item.from_data(data['Item'], key_schema)
end

#check_conditions(old_item, conditions) ⇒ Object



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/fake_dynamo/table.rb', line 366

def check_conditions(old_item, conditions)
  return unless conditions

  conditions.each do |name, predicate|
    exist = predicate['Exists']
    value = predicate['Value']

    if not value
      if exist.nil?
        raise ValidationException, "'Exists' is set to null. 'Exists' must be set to false when no Attribute value is specified"
      elsif exist
        raise ValidationException, "'Exists' is set to true. 'Exists' must be set to false when no Attribute value is specified"
      elsif !exist # false
        if old_item and old_item[name]
          raise ConditionalCheckFailedException
        end
      end
    else
      expected_attr = Attribute.from_hash(name, value)

      if exist.nil? or exist
        raise ConditionalCheckFailedException unless (old_item and old_item[name] == expected_attr)
      elsif !exist # false
        raise ValidationException, "Cannot expect an attribute to have a specified value while expecting it to not exist"
      end
    end
  end
end

#consumed_capacityObject



362
363
364
# File 'lib/fake_dynamo/table.rb', line 362

def consumed_capacity
  { 'ConsumedCapacityUnits' => 1 }
end

#count_and_attributes_to_get_present?(data) ⇒ Boolean

Returns:

  • (Boolean)


250
251
252
253
254
# File 'lib/fake_dynamo/table.rb', line 250

def count_and_attributes_to_get_present?(data)
  if data['Count'] and data['AttributesToGet']
    raise ValidationException, "Cannot specify the AttributesToGet when choosing to get only the Count"
  end
end

#create_item?(data) ⇒ Boolean

Returns:

  • (Boolean)


318
319
320
321
322
323
# File 'lib/fake_dynamo/table.rb', line 318

def create_item?(data)
  data['AttributeUpdates'].any? do |name, update_data|
    action = update_data['Action']
    ['PUT', 'ADD', nil].include? action
  end
end

#create_table_dataObject



30
31
32
33
34
35
36
37
38
39
# File 'lib/fake_dynamo/table.rb', line 30

def create_table_data
  {
    'TableName' => name,
    'KeySchema' => key_schema.description,
    'ProvisionedThroughput' => {
      'ReadCapacityUnits' => read_capacity_units,
      'WriteCapacityUnits' => write_capacity_units
    }
  }
end

#deep_copy(x) ⇒ Object



188
189
190
# File 'lib/fake_dynamo/table.rb', line 188

def deep_copy(x)
  Marshal.load(Marshal.dump(x))
end

#deleteObject



61
62
63
64
# File 'lib/fake_dynamo/table.rb', line 61

def delete
  @status = 'DELETING'
  description
end

#delete_item(data) ⇒ Object



139
140
141
142
143
144
145
146
# File 'lib/fake_dynamo/table.rb', line 139

def delete_item(data)
  key = Key.from_data(data['Key'], key_schema)
  item = @items[key]
  check_conditions(item, data['Expected'])

  @items.delete(key) if item
  consumed_capacity.merge(return_values(data, item))
end

#describe_tableObject



53
54
55
# File 'lib/fake_dynamo/table.rb', line 53

def describe_table
  { 'Table' => description['TableDescription'] }.merge(size_description)
end

#descriptionObject



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/fake_dynamo/table.rb', line 15

def description
  {
    'TableDescription' => {
      'CreationDateTime' => creation_date_time,
      'KeySchema' => key_schema.description,
      'ProvisionedThroughput' => {
        'ReadCapacityUnits' => read_capacity_units,
        'WriteCapacityUnits' => write_capacity_units
      },
      'TableName' => name,
      'TableStatus' => status
    }
  }
end

#drop_till_start(all_items, start_key_hash, forward) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/fake_dynamo/table.rb', line 262

def drop_till_start(all_items, start_key_hash, forward)
  all_items = all_items.sort_by { |item| item.key }

  unless forward
    all_items = all_items.reverse
  end

  if start_key_hash
    start_key = Key.from_data(start_key_hash, key_schema)
    all_items.drop_while do |item|
      if forward
        item.key <= start_key
      else
        item.key >= start_key
      end
    end
  else
    all_items
  end
end

#filter(items, conditions, limit, fail_on_type_mismatch) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/fake_dynamo/table.rb', line 283

def filter(items, conditions, limit, fail_on_type_mismatch)
  limit ||= -1
  result = []
  last_evaluated_item = nil
  scaned_count = 0
  items.each do |item|
    select = true
    conditions.each do |attribute_name, condition|
      value = condition['AttributeValueList']
      comparison_op = condition['ComparisonOperator']
      unless self.send("#{comparison_op.downcase}_filter", value, item[attribute_name], fail_on_type_mismatch)
        select = false
        break
      end
    end

    if select
      result << item
      if (limit -= 1) == 0
        last_evaluated_item = item
        break
      end
    end

    scaned_count += 1
  end
  [result, last_evaluated_item, scaned_count]
end

#filter_attributes(item, attributes_to_get) ⇒ Object



129
130
131
132
133
134
135
136
137
# File 'lib/fake_dynamo/table.rb', line 129

def filter_attributes(item, attributes_to_get)
  hash = item.as_hash
  if attributes_to_get
    hash.select! do |attribute, value|
      attributes_to_get.include? attribute
    end
  end
  hash
end

#get_item(data) ⇒ Object



112
113
114
115
116
117
118
# File 'lib/fake_dynamo/table.rb', line 112

def get_item(data)
  response = consumed_capacity
  if item_hash = get_raw_item(data['Key'], data['AttributesToGet'])
    response.merge!('Item' => item_hash)
  end
  response
end

#get_items_by_hash_key(hash_key) ⇒ Object



312
313
314
315
316
# File 'lib/fake_dynamo/table.rb', line 312

def get_items_by_hash_key(hash_key)
  items.values.select do |i|
    i.key.primary == hash_key
  end
end

#get_raw_item(key_data, attributes_to_get) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/fake_dynamo/table.rb', line 120

def get_raw_item(key_data, attributes_to_get)
  key = Key.from_data(key_data, key_schema)
  item = @items[key]

  if item
    filter_attributes(item, attributes_to_get)
  end
end

#put_item(data) ⇒ Object



95
96
97
98
99
100
101
102
# File 'lib/fake_dynamo/table.rb', line 95

def put_item(data)
  item = Item.from_data(data['Item'], key_schema)
  old_item = @items[item.key]
  check_conditions(old_item, data['Expected'])
  @items[item.key] = item

  consumed_capacity.merge(return_values(data, old_item))
end

#put_item_data(item) ⇒ Object



41
42
43
44
45
46
# File 'lib/fake_dynamo/table.rb', line 41

def put_item_data(item)
  {
    'TableName' => name,
    'Item' => item.as_hash
  }
end

#query(data) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/fake_dynamo/table.rb', line 192

def query(data)
  unless key_schema.range_key
    raise ValidationException, "Query can be performed only on a table with a HASH,RANGE key schema"
  end

  count_and_attributes_to_get_present?(data)
  validate_limit(data)

  hash_attribute = Attribute.from_hash(key_schema.hash_key.name, data['HashKeyValue'])
  matched_items = get_items_by_hash_key(hash_attribute)

  forward = data.has_key?('ScanIndexForward') ? data['ScanIndexForward'] : true
  matched_items = drop_till_start(matched_items, data['ExclusiveStartKey'], forward)

  if data['RangeKeyCondition']
    conditions = {key_schema.range_key.name => data['RangeKeyCondition']}
  else
    conditions = {}
  end

  result, last_evaluated_item, _ = filter(matched_items, conditions, data['Limit'], true)

  response = {
    'Count' => result.size,
    'ConsumedCapacityUnits' => 1 }

  unless data['Count']
    response['Items'] = result.map { |r| filter_attributes(r, data['AttributesToGet']) }
  end

  if last_evaluated_item
    response['LastEvaluatedKey'] = last_evaluated_item.key.as_key_hash
  end
  response
end

#return_values(data, old_item, new_item = {}) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/fake_dynamo/table.rb', line 329

def return_values(data, old_item, new_item={})
  old_item ||= {}
  old_hash = old_item.kind_of?(Item) ? old_item.as_hash : old_item

  new_item ||= {}
  new_hash = new_item.kind_of?(Item) ? new_item.as_hash : new_item


  return_value = data['ReturnValues']
  result = case return_value
           when 'ALL_OLD'
             old_hash
           when 'ALL_NEW'
             new_hash
           when 'UPDATED_OLD'
             updated = updated_attributes(data)
             old_hash.select { |name, _| updated.include? name }
           when 'UPDATED_NEW'
             updated = updated_attributes(data)
             new_hash.select { |name, _| updated.include? name }
           when 'NONE', nil
             {}
           else
             raise 'unknown return value'
           end

  unless result.empty?
    { 'Attributes' => result }
  else
    {}
  end
end

#scan(data) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/fake_dynamo/table.rb', line 228

def scan(data)
  count_and_attributes_to_get_present?(data)
  validate_limit(data)
  conditions = data['ScanFilter'] || {}
  all_items = drop_till_start(items.values, data['ExclusiveStartKey'], true)
  result, last_evaluated_item, scaned_count = filter(all_items, conditions, data['Limit'], false)
  response = {
    'Count' => result.size,
    'ScannedCount' => scaned_count,
    'ConsumedCapacityUnits' => 1 }

  unless data['Count']
    response['Items'] = result.map { |r| filter_attributes(r, data['AttributesToGet']) }
  end

  if last_evaluated_item
    response['LastEvaluatedKey'] = last_evaluated_item.key.as_key_hash
  end

  response
end

#size_descriptionObject



48
49
50
51
# File 'lib/fake_dynamo/table.rb', line 48

def size_description
  { 'ItemCount' => items.count,
    'TableSizeBytes' => size_bytes }
end

#update(read_capacity_units, write_capacity_units) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/fake_dynamo/table.rb', line 66

def update(read_capacity_units, write_capacity_units)
  if @read_capacity_units > read_capacity_units
    @last_decreased_time = Time.now.to_i
  elsif @read_capacity_units < read_capacity_units
    @last_increased_time = Time.now.to_i
  end

  if @write_capacity_units > write_capacity_units
    @last_decreased_time = Time.now.to_i
  elsif @write_capacity_units < write_capacity_units
    @last_increased_time = Time.now.to_i
  end

  @read_capacity_units, @write_capacity_units = read_capacity_units, write_capacity_units

  response = description.merge(size_description)

  if last_increased_time
    response['TableDescription']['ProvisionedThroughput']['LastIncreaseDateTime'] = @last_increased_time
  end

  if last_decreased_time
    response['TableDescription']['ProvisionedThroughput']['LastDecreaseDateTime'] = @last_decreased_time
  end

  response['TableDescription']['TableStatus'] = 'UPDATING'
  response
end

#update_item(data) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/fake_dynamo/table.rb', line 156

def update_item(data)
  key = Key.from_data(data['Key'], key_schema)
  item = @items[key]
  check_conditions(item, data['Expected'])

  unless item
    if create_item?(data)
      item = @items[key] = Item.from_key(key)
    else
      return consumed_capacity
    end
    item_created = true
  end

  old_item = deep_copy(item)
  begin
    old_hash = item.as_hash
    data['AttributeUpdates'].each do |name, update_data|
      item.update(name, update_data)
    end
  rescue => e
    if item_created
      @items.delete(key)
    else
      @items[key] = old_item
    end
    raise e
  end

  consumed_capacity.merge(return_values(data, old_hash, item))
end

#updated_attributes(data) ⇒ Object



325
326
327
# File 'lib/fake_dynamo/table.rb', line 325

def updated_attributes(data)
  data['AttributeUpdates'].map { |name, _| name }
end

#validate_limit(data) ⇒ Object



256
257
258
259
260
# File 'lib/fake_dynamo/table.rb', line 256

def validate_limit(data)
  if data['Limit'] and data['Limit'] <= 0
    raise ValidationException, "Limit failed to satisfy constraint: Member must have value greater than or equal to 1"
  end
end