Merge pull request #2509 from bblommers/feature/847

DynamoDB - Support list_append operation
This commit is contained in:
Jack Danger 2019-10-23 15:20:37 -07:00 committed by GitHub
commit 61f153f168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 9 deletions

View File

@ -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

View File

@ -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
@ -1323,7 +1323,7 @@ def test_bad_scan_filter():
except ClientError as err:
err.response['Error']['Code'].should.equal('ValidationError')
else:
raise RuntimeError('Should of raised ResourceInUseException')
raise RuntimeError('Should have raised ResourceInUseException')
@mock_dynamodb2
@ -1375,7 +1375,7 @@ def test_duplicate_create():
except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceInUseException')
else:
raise RuntimeError('Should of raised ResourceInUseException')
raise RuntimeError('Should have raised ResourceInUseException')
@mock_dynamodb2
@ -1400,7 +1400,7 @@ def test_delete_table():
except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceNotFoundException')
else:
raise RuntimeError('Should of raised ResourceNotFoundException')
raise RuntimeError('Should have raised ResourceNotFoundException')
@mock_dynamodb2
@ -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,74 @@ 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
str(ex.exception).should.match("Parameter validation failed:")
str(ex.exception).should.match("Invalid type for parameter ExpressionAttributeValues.")
def _create_user_table():