Home | History | Annotate | Download | only in dynamodb
      1 # Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
      2 # All rights reserved.
      3 #
      4 # Permission is hereby granted, free of charge, to any person obtaining a
      5 # copy of this software and associated documentation files (the
      6 # "Software"), to deal in the Software without restriction, including
      7 # without limitation the rights to use, copy, modify, merge, publish, dis-
      8 # tribute, sublicense, and/or sell copies of the Software, and to permit
      9 # persons to whom the Software is furnished to do so, subject to the fol-
     10 # lowing conditions:
     11 #
     12 # The above copyright notice and this permission notice shall be included
     13 # in all copies or substantial portions of the Software.
     14 #
     15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
     16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
     17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
     18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
     19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
     21 # IN THE SOFTWARE.
     22 
     23 """
     24 Tests for Layer2 of Amazon DynamoDB
     25 """
     26 import time
     27 import uuid
     28 from decimal import Decimal
     29 
     30 from tests.unit import unittest
     31 from boto.dynamodb.exceptions import DynamoDBKeyNotFoundError
     32 from boto.dynamodb.exceptions import DynamoDBConditionalCheckFailedError
     33 from boto.dynamodb.layer2 import Layer2
     34 from boto.dynamodb.types import get_dynamodb_type, Binary
     35 from boto.dynamodb.condition import BEGINS_WITH, CONTAINS, GT
     36 from boto.compat import six, long_type
     37 
     38 
     39 class DynamoDBLayer2Test(unittest.TestCase):
     40     dynamodb = True
     41 
     42     def setUp(self):
     43         self.dynamodb = Layer2()
     44         self.hash_key_name = 'forum_name'
     45         self.hash_key_proto_value = ''
     46         self.range_key_name = 'subject'
     47         self.range_key_proto_value = ''
     48         self.table_name = 'sample_data_%s' % int(time.time())
     49 
     50     def create_sample_table(self):
     51         schema = self.dynamodb.create_schema(
     52             self.hash_key_name, self.hash_key_proto_value,
     53             self.range_key_name,
     54             self.range_key_proto_value)
     55         table = self.create_table(self.table_name, schema, 5, 5)
     56         table.refresh(wait_for_active=True)
     57         return table
     58 
     59     def create_table(self, table_name, schema, read_units, write_units):
     60         result = self.dynamodb.create_table(table_name, schema, read_units, write_units)
     61         self.addCleanup(self.dynamodb.delete_table, result)
     62         return result
     63 
     64     def test_layer2_basic(self):
     65         print('--- running Amazon DynamoDB Layer2 tests ---')
     66         c = self.dynamodb
     67 
     68         # First create a schema for the table
     69         schema = c.create_schema(self.hash_key_name, self.hash_key_proto_value,
     70                                  self.range_key_name,
     71                                  self.range_key_proto_value)
     72 
     73         # Create another schema without a range key
     74         schema2 = c.create_schema('post_id', '')
     75 
     76         # Now create a table
     77         index = int(time.time())
     78         table_name = 'test-%d' % index
     79         read_units = 5
     80         write_units = 5
     81         table = self.create_table(table_name, schema, read_units, write_units)
     82         assert table.name == table_name
     83         assert table.schema.hash_key_name == self.hash_key_name
     84         assert table.schema.hash_key_type == get_dynamodb_type(self.hash_key_proto_value)
     85         assert table.schema.range_key_name == self.range_key_name
     86         assert table.schema.range_key_type == get_dynamodb_type(self.range_key_proto_value)
     87         assert table.read_units == read_units
     88         assert table.write_units == write_units
     89         assert table.item_count == 0
     90         assert table.size_bytes == 0
     91 
     92         # Create the second table
     93         table2_name = 'test-%d' % (index + 1)
     94         table2 = self.create_table(table2_name, schema2, read_units, write_units)
     95 
     96         # Wait for table to become active
     97         table.refresh(wait_for_active=True)
     98         table2.refresh(wait_for_active=True)
     99 
    100         # List tables and make sure new one is there
    101         table_names = c.list_tables()
    102         assert table_name in table_names
    103         assert table2_name in table_names
    104 
    105         # Update the tables ProvisionedThroughput
    106         new_read_units = 10
    107         new_write_units = 5
    108         table.update_throughput(new_read_units, new_write_units)
    109 
    110         # Wait for table to be updated
    111         table.refresh(wait_for_active=True)
    112         assert table.read_units == new_read_units
    113         assert table.write_units == new_write_units
    114 
    115         # Put an item
    116         item1_key = 'Amazon DynamoDB'
    117         item1_range = 'DynamoDB Thread 1'
    118         item1_attrs = {
    119             'Message': 'DynamoDB thread 1 message text',
    120             'LastPostedBy': 'User A',
    121             'Views': 0,
    122             'Replies': 0,
    123             'Answered': 0,
    124             'Public': True,
    125             'Tags': set(['index', 'primarykey', 'table']),
    126             'LastPostDateTime': '12/9/2011 11:36:03 PM'}
    127 
    128         # Test a few corner cases with new_item
    129 
    130         # Try supplying a hash_key as an arg and as an item in attrs
    131         item1_attrs[self.hash_key_name] = 'foo'
    132         foobar_item = table.new_item(item1_key, item1_range, item1_attrs)
    133         assert foobar_item.hash_key == item1_key
    134 
    135         # Try supplying a range_key as an arg and as an item in attrs
    136         item1_attrs[self.range_key_name] = 'bar'
    137         foobar_item = table.new_item(item1_key, item1_range, item1_attrs)
    138         assert foobar_item.range_key == item1_range
    139 
    140         # Try supplying hash and range key in attrs dict
    141         foobar_item = table.new_item(attrs=item1_attrs)
    142         assert foobar_item.hash_key == 'foo'
    143         assert foobar_item.range_key == 'bar'
    144 
    145         del item1_attrs[self.hash_key_name]
    146         del item1_attrs[self.range_key_name]
    147 
    148         item1 = table.new_item(item1_key, item1_range, item1_attrs)
    149         # make sure the put() succeeds
    150         try:
    151             item1.put()
    152         except c.layer1.ResponseError as e:
    153             raise Exception("Item put failed: %s" % e)
    154 
    155         # Try to get an item that does not exist.
    156         self.assertRaises(DynamoDBKeyNotFoundError,
    157                           table.get_item, 'bogus_key', item1_range)
    158 
    159         # Now do a consistent read and check results
    160         item1_copy = table.get_item(item1_key, item1_range,
    161                                     consistent_read=True)
    162         assert item1_copy.hash_key == item1.hash_key
    163         assert item1_copy.range_key == item1.range_key
    164         for attr_name in item1_attrs:
    165             val = item1_copy[attr_name]
    166             if isinstance(val, (int, long_type, float, six.string_types)):
    167                 assert val == item1[attr_name]
    168 
    169         # Try retrieving only select attributes
    170         attributes = ['Message', 'Views']
    171         item1_small = table.get_item(item1_key, item1_range,
    172                                      attributes_to_get=attributes,
    173                                      consistent_read=True)
    174         for attr_name in item1_small:
    175             # The item will include the attributes we asked for as
    176             # well as the hashkey and rangekey, so filter those out.
    177             if attr_name not in (item1_small.hash_key_name,
    178                                  item1_small.range_key_name):
    179                 assert attr_name in attributes
    180 
    181         self.assertTrue(table.has_item(item1_key, range_key=item1_range,
    182                                        consistent_read=True))
    183 
    184         # Try to delete the item with the wrong Expected value
    185         expected = {'Views': 1}
    186         self.assertRaises(DynamoDBConditionalCheckFailedError,
    187                           item1.delete, expected_value=expected)
    188 
    189         # Try to delete a value while expecting a non-existant attribute
    190         expected = {'FooBar': True}
    191         try:
    192             item1.delete(expected_value=expected)
    193         except c.layer1.ResponseError:
    194             pass
    195 
    196         # Now update the existing object
    197         item1.add_attribute('Replies', 2)
    198 
    199         removed_attr = 'Public'
    200         item1.delete_attribute(removed_attr)
    201 
    202         removed_tag = item1_attrs['Tags'].copy().pop()
    203         item1.delete_attribute('Tags', set([removed_tag]))
    204 
    205         replies_by_set = set(['Adam', 'Arnie'])
    206         item1.put_attribute('RepliesBy', replies_by_set)
    207         retvals = item1.save(return_values='ALL_OLD')
    208         # Need more tests here for variations on return_values
    209         assert 'Attributes' in retvals
    210 
    211         # Check for correct updates
    212         item1_updated = table.get_item(item1_key, item1_range,
    213                                        consistent_read=True)
    214         assert item1_updated['Replies'] == item1_attrs['Replies'] + 2
    215         self.assertFalse(removed_attr in item1_updated)
    216         self.assertTrue(removed_tag not in item1_updated['Tags'])
    217         self.assertTrue('RepliesBy' in item1_updated)
    218         self.assertTrue(item1_updated['RepliesBy'] == replies_by_set)
    219 
    220         # Put a few more items into the table
    221         item2_key = 'Amazon DynamoDB'
    222         item2_range = 'DynamoDB Thread 2'
    223         item2_attrs = {
    224             'Message': 'DynamoDB thread 2 message text',
    225             'LastPostedBy': 'User A',
    226             'Views': 0,
    227             'Replies': 0,
    228             'Answered': 0,
    229             'Tags': set(["index", "primarykey", "table"]),
    230             'LastPost2DateTime': '12/9/2011 11:36:03 PM'}
    231         item2 = table.new_item(item2_key, item2_range, item2_attrs)
    232         item2.put()
    233 
    234         item3_key = 'Amazon S3'
    235         item3_range = 'S3 Thread 1'
    236         item3_attrs = {
    237             'Message': 'S3 Thread 1 message text',
    238             'LastPostedBy': 'User A',
    239             'Views': 0,
    240             'Replies': 0,
    241             'Answered': 0,
    242             'Tags': set(['largeobject', 'multipart upload']),
    243             'LastPostDateTime': '12/9/2011 11:36:03 PM'
    244         }
    245         item3 = table.new_item(item3_key, item3_range, item3_attrs)
    246         item3.put()
    247 
    248         # Put an item into the second table
    249         table2_item1_key = uuid.uuid4().hex
    250         table2_item1_attrs = {
    251             'DateTimePosted': '25/1/2011 12:34:56 PM',
    252             'Text': 'I think boto rocks and so does DynamoDB'
    253         }
    254         table2_item1 = table2.new_item(table2_item1_key,
    255                                        attrs=table2_item1_attrs)
    256         table2_item1.put()
    257 
    258         # Try a few queries
    259         items = table.query('Amazon DynamoDB', range_key_condition=BEGINS_WITH('DynamoDB'))
    260         n = 0
    261         for item in items:
    262             n += 1
    263         assert n == 2
    264         assert items.consumed_units > 0
    265 
    266         items = table.query('Amazon DynamoDB', range_key_condition=BEGINS_WITH('DynamoDB'),
    267                             request_limit=1, max_results=1)
    268         n = 0
    269         for item in items:
    270             n += 1
    271         assert n == 1
    272         assert items.consumed_units > 0
    273 
    274         # Try a few scans
    275         items = table.scan()
    276         n = 0
    277         for item in items:
    278             n += 1
    279         assert n == 3
    280         assert items.consumed_units > 0
    281 
    282         items = table.scan(scan_filter={'Replies': GT(0)})
    283         n = 0
    284         for item in items:
    285             n += 1
    286         assert n == 1
    287         assert items.consumed_units > 0
    288 
    289         # Test some integer and float attributes
    290         integer_value = 42
    291         float_value = 345.678
    292         item3['IntAttr'] = integer_value
    293         item3['FloatAttr'] = float_value
    294 
    295         # Test booleans
    296         item3['TrueBoolean'] = True
    297         item3['FalseBoolean'] = False
    298 
    299         # Test some set values
    300         integer_set = set([1, 2, 3, 4, 5])
    301         float_set = set([1.1, 2.2, 3.3, 4.4, 5.5])
    302         mixed_set = set([1, 2, 3.3, 4, 5.555])
    303         str_set = set(['foo', 'bar', 'fie', 'baz'])
    304         item3['IntSetAttr'] = integer_set
    305         item3['FloatSetAttr'] = float_set
    306         item3['MixedSetAttr'] = mixed_set
    307         item3['StrSetAttr'] = str_set
    308         item3.put()
    309 
    310         # Now do a consistent read
    311         item4 = table.get_item(item3_key, item3_range, consistent_read=True)
    312         assert item4['IntAttr'] == integer_value
    313         assert item4['FloatAttr'] == float_value
    314         assert bool(item4['TrueBoolean']) is True
    315         assert bool(item4['FalseBoolean']) is False
    316         # The values will not necessarily be in the same order as when
    317         # we wrote them to the DB.
    318         for i in item4['IntSetAttr']:
    319             assert i in integer_set
    320         for i in item4['FloatSetAttr']:
    321             assert i in float_set
    322         for i in item4['MixedSetAttr']:
    323             assert i in mixed_set
    324         for i in item4['StrSetAttr']:
    325             assert i in str_set
    326 
    327         # Try a batch get
    328         batch_list = c.new_batch_list()
    329         batch_list.add_batch(table, [(item2_key, item2_range),
    330                                      (item3_key, item3_range)])
    331         response = batch_list.submit()
    332         assert len(response['Responses'][table.name]['Items']) == 2
    333 
    334         # Try an empty batch get
    335         batch_list = c.new_batch_list()
    336         batch_list.add_batch(table, [])
    337         response = batch_list.submit()
    338         assert response == {}
    339 
    340         # Try a few batch write operations
    341         item4_key = 'Amazon S3'
    342         item4_range = 'S3 Thread 2'
    343         item4_attrs = {
    344             'Message': 'S3 Thread 2 message text',
    345             'LastPostedBy': 'User A',
    346             'Views': 0,
    347             'Replies': 0,
    348             'Answered': 0,
    349             'Tags': set(['largeobject', 'multipart upload']),
    350             'LastPostDateTime': '12/9/2011 11:36:03 PM'
    351         }
    352         item5_key = 'Amazon S3'
    353         item5_range = 'S3 Thread 3'
    354         item5_attrs = {
    355             'Message': 'S3 Thread 3 message text',
    356             'LastPostedBy': 'User A',
    357             'Views': 0,
    358             'Replies': 0,
    359             'Answered': 0,
    360             'Tags': set(['largeobject', 'multipart upload']),
    361             'LastPostDateTime': '12/9/2011 11:36:03 PM'
    362         }
    363         item4 = table.new_item(item4_key, item4_range, item4_attrs)
    364         item5 = table.new_item(item5_key, item5_range, item5_attrs)
    365         batch_list = c.new_batch_write_list()
    366         batch_list.add_batch(table, puts=[item4, item5])
    367         response = batch_list.submit()
    368         # should really check for unprocessed items
    369 
    370         # Do some generator gymnastics
    371         results = table.scan(scan_filter={'Tags': CONTAINS('table')})
    372         assert results.scanned_count == 5
    373         results = table.scan(request_limit=2, max_results=5)
    374         assert results.count == 2
    375         for item in results:
    376             if results.count == 2:
    377                 assert results.remaining == 4
    378                 results.remaining -= 2
    379                 results.next_response()
    380             else:
    381                 assert results.count == 4
    382                 assert results.remaining in (0, 1)
    383         assert results.count == 4
    384         results = table.scan(request_limit=6, max_results=4)
    385         assert len(list(results)) == 4
    386         assert results.count == 4
    387 
    388         batch_list = c.new_batch_write_list()
    389         batch_list.add_batch(table, deletes=[(item4_key, item4_range),
    390                                              (item5_key, item5_range)])
    391         response = batch_list.submit()
    392 
    393         # Try queries
    394         results = table.query('Amazon DynamoDB', range_key_condition=BEGINS_WITH('DynamoDB'))
    395         n = 0
    396         for item in results:
    397             n += 1
    398         assert n == 2
    399 
    400         # Try to delete the item with the right Expected value
    401         expected = {'Views': 0}
    402         item1.delete(expected_value=expected)
    403 
    404         self.assertFalse(table.has_item(item1_key, range_key=item1_range,
    405                                         consistent_read=True))
    406         # Now delete the remaining items
    407         ret_vals = item2.delete(return_values='ALL_OLD')
    408         # some additional checks here would be useful
    409         assert ret_vals['Attributes'][self.hash_key_name] == item2_key
    410         assert ret_vals['Attributes'][self.range_key_name] == item2_range
    411 
    412         item3.delete()
    413         table2_item1.delete()
    414         print('--- tests completed ---')
    415 
    416     def test_binary_attrs(self):
    417         c = self.dynamodb
    418         schema = c.create_schema(self.hash_key_name, self.hash_key_proto_value,
    419                                  self.range_key_name,
    420                                  self.range_key_proto_value)
    421         index = int(time.time())
    422         table_name = 'test-%d' % index
    423         read_units = 5
    424         write_units = 5
    425         table = self.create_table(table_name, schema, read_units, write_units)
    426         table.refresh(wait_for_active=True)
    427         item1_key = 'Amazon S3'
    428         item1_range = 'S3 Thread 1'
    429         item1_attrs = {
    430             'Message': 'S3 Thread 1 message text',
    431             'LastPostedBy': 'User A',
    432             'Views': 0,
    433             'Replies': 0,
    434             'Answered': 0,
    435             'BinaryData': Binary(b'\x01\x02\x03\x04'),
    436             'BinarySequence': set([Binary(b'\x01\x02'), Binary(b'\x03\x04')]),
    437             'Tags': set(['largeobject', 'multipart upload']),
    438             'LastPostDateTime': '12/9/2011 11:36:03 PM'
    439         }
    440         item1 = table.new_item(item1_key, item1_range, item1_attrs)
    441         item1.put()
    442 
    443         retrieved = table.get_item(item1_key, item1_range, consistent_read=True)
    444         self.assertEqual(retrieved['Message'], 'S3 Thread 1 message text')
    445         self.assertEqual(retrieved['Views'], 0)
    446         self.assertEqual(retrieved['Tags'],
    447                          set(['largeobject', 'multipart upload']))
    448         self.assertEqual(retrieved['BinaryData'], Binary(b'\x01\x02\x03\x04'))
    449         # Also comparable directly to bytes:
    450         self.assertEqual(retrieved['BinaryData'], b'\x01\x02\x03\x04')
    451         self.assertEqual(retrieved['BinarySequence'],
    452                          set([Binary(b'\x01\x02'), Binary(b'\x03\x04')]))
    453 
    454     def test_put_decimal_attrs(self):
    455         self.dynamodb.use_decimals()
    456         table = self.create_sample_table()
    457         item = table.new_item('foo', 'bar')
    458         item['decimalvalue'] = Decimal('1.12345678912345')
    459         item.put()
    460         retrieved = table.get_item('foo', 'bar')
    461         self.assertEqual(retrieved['decimalvalue'], Decimal('1.12345678912345'))
    462 
    463     @unittest.skipIf(six.PY3, "skipping lossy_float_conversion test for Python 3.x")
    464     def test_lossy_float_conversion(self):
    465         table = self.create_sample_table()
    466         item = table.new_item('foo', 'bar')
    467         item['floatvalue'] = 1.12345678912345
    468         item.put()
    469         retrieved = table.get_item('foo', 'bar')['floatvalue']
    470         # Notice how this is not equal to the original value.
    471         self.assertNotEqual(1.12345678912345, retrieved)
    472         # Instead, it's truncated:
    473         self.assertEqual(1.12345678912, retrieved)
    474 
    475     def test_large_integers(self):
    476         # It's not just floating point numbers, large integers
    477         # can trigger rouding issues.
    478         self.dynamodb.use_decimals()
    479         table = self.create_sample_table()
    480         item = table.new_item('foo', 'bar')
    481         item['decimalvalue'] = Decimal('129271300103398600')
    482         item.put()
    483         retrieved = table.get_item('foo', 'bar')
    484         self.assertEqual(retrieved['decimalvalue'], Decimal('129271300103398600'))
    485         # Also comparable directly to an int.
    486         self.assertEqual(retrieved['decimalvalue'], 129271300103398600)
    487 
    488     def test_put_single_letter_attr(self):
    489         # When an attr is added that is a single letter, if it overlaps with
    490         # the built-in "types", the decoding used to fall down. Assert that
    491         # it's now working correctly.
    492         table = self.create_sample_table()
    493         item = table.new_item('foo', 'foo1')
    494         item.put_attribute('b', 4)
    495         stored = item.save(return_values='UPDATED_NEW')
    496         self.assertEqual(stored['Attributes'], {'b': 4})
    497