diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index d4704bad7..040196828 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -9,6 +9,7 @@ import uuid import six import boto3 +from botocore.exceptions import ParamValidationError from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time @@ -302,6 +303,8 @@ class Item(BaseModel): attr, list_index = attribute_is_list(key.split('.')[0]) # If value not exists, changes value to a default if needed, else its the same as it was value = self._get_default(value) + # If operation == list_append, get the original value and append it + value = self._get_appended_list(value, expression_attribute_values) if type(value) != DynamoType: if value in expression_attribute_values: @@ -370,6 +373,18 @@ class Item(BaseModel): else: raise NotImplementedError('{} update action not yet supported'.format(action)) + def _get_appended_list(self, value, expression_attribute_values): + if type(value) != DynamoType: + list_append_re = re.match('list_append\\((.+),(.+)\\)', value) + if list_append_re: + new_value = expression_attribute_values[list_append_re.group(2).strip()] + old_list = self.attrs[list_append_re.group(1)] + if not old_list.is_list(): + raise ParamValidationError + old_list.value.extend(new_value['L']) + value = old_list + return value + def _get_default(self, value): if value.startswith('if_not_exists'): # Function signature diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 08ba6428c..a2b97ff08 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -11,7 +11,7 @@ import requests from moto import mock_dynamodb2, mock_dynamodb2_deprecated from moto.dynamodb2 import dynamodb_backend2 from boto.exception import JSONResponseError -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, ParamValidationError from tests.helpers import requires_boto_gte import tests.backport_assert_raises @@ -2734,6 +2734,13 @@ def test_item_size_is_under_400KB(): Item={'id': {'S': 'foo'}, 'itemlist': {'L': [{'M': {'item1': {'S': large_item}}}]}}) +def assert_failure_due_to_item_size(func, **kwargs): + with assert_raises(ClientError) as ex: + func(**kwargs) + ex.exception.response['Error']['Code'].should.equal('ValidationException') + ex.exception.response['Error']['Message'].should.equal('Item size has exceeded the maximum allowed size') + + @mock_dynamodb2 # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression def test_hash_key_cannot_use_begins_with_operations(): @@ -2759,11 +2766,75 @@ def test_hash_key_cannot_use_begins_with_operations(): ex.exception.response['Error']['Message'].should.equal('Query key condition not supported') -def assert_failure_due_to_item_size(func, **kwargs): - with assert_raises(ClientError) as ex: - func(**kwargs) - ex.exception.response['Error']['Code'].should.equal('ValidationException') - ex.exception.response['Error']['Message'].should.equal('Item size has exceeded the maximum allowed size') +@mock_dynamodb2 +def test_update_supports_complex_expression_attribute_values(): + client = boto3.client('dynamodb') + + client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}], + TableName='TestTable', + KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + + client.update_item(TableName='TestTable', + Key={'SHA256': {'S': 'sha-of-file'}}, + UpdateExpression=('SET MD5 = :md5,' + 'MyStringSet = :string_set,' + 'MyMap = :map' ), + ExpressionAttributeValues={':md5': {'S': 'md5-of-file'}, + ':string_set': {'SS': ['string1', 'string2']}, + ':map': {'M': {'EntryKey': {'SS': ['thing1', 'thing2']}}}}) + result = client.get_item(TableName='TestTable', Key={'SHA256': {'S': 'sha-of-file'}})['Item'] + result.should.equal({u'MyStringSet': {u'SS': [u'string1', u'string2']}, + 'MyMap': {u'M': {u'EntryKey': {u'SS': [u'thing1', u'thing2']}}}, + 'SHA256': {u'S': u'sha-of-file'}, + 'MD5': {u'S': u'md5-of-file'}}) + + +@mock_dynamodb2 +def test_update_supports_list_append(): + client = boto3.client('dynamodb') + + client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}], + TableName='TestTable', + KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + client.put_item(TableName='TestTable', + Item={'SHA256': {'S': 'sha-of-file'}, 'crontab': {'L': [{'S': 'bar1'}]}}) + + # Update item using list_append expression + client.update_item(TableName='TestTable', + Key={'SHA256': {'S': 'sha-of-file'}}, + UpdateExpression="SET crontab = list_append(crontab, :i)", + ExpressionAttributeValues={':i': {'L': [{'S': 'bar2'}]}}) + + # Verify item is appended to the existing list + result = client.get_item(TableName='TestTable', Key={'SHA256': {'S': 'sha-of-file'}})['Item'] + result.should.equal({'SHA256': {'S': 'sha-of-file'}, + 'crontab': {'L': [{'S': 'bar1'}, {'S': 'bar2'}]}}) + + +@mock_dynamodb2 +def test_update_catches_invalid_list_append_operation(): + client = boto3.client('dynamodb') + + client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}], + TableName='TestTable', + KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + client.put_item(TableName='TestTable', + Item={'SHA256': {'S': 'sha-of-file'}, 'crontab': {'L': [{'S': 'bar1'}]}}) + + # Update item using invalid list_append expression + with assert_raises(ParamValidationError) as ex: + client.update_item(TableName='TestTable', + Key={'SHA256': {'S': 'sha-of-file'}}, + UpdateExpression="SET crontab = list_append(crontab, :i)", + ExpressionAttributeValues={':i': [{'S': 'bar2'}]}) + + # Verify correct error is returned + ex.exception.message.should.equal("Parameter validation failed:\n" + "Invalid type for parameter ExpressionAttributeValues." + ":i, value: [{u'S': u'bar2'}], type: , valid types: ") def _create_user_table():